diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8304697..0e3898a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/post-for-me-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -46,7 +46,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/post-for-me-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | @@ -67,7 +67,7 @@ jobs: github.repository == 'stainless-sdks/post-for-me-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -87,7 +87,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/post-for-me-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b515eb6..a1d25b3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index df36800..0044a6a 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'DayMoonDevelopment/post-for-me-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7ccfe12..bc845f3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.15.0" + ".": "1.16.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 0a2314b..f6bd627 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/day-moon-development%2Fpost-for-me-d7bde21e6d3328e90ec781ff8e2629faeaae4bb5d8e0d350703326ec8aadf898.yml -openapi_spec_hash: dcb2130480c4476fe08fcb080e369ce0 -config_hash: 0ec19602e41aea0526548245a59d4253 +configured_endpoints: 21 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/day-moon-development/post-for-me-b4207c01320058903d7a081df1d83e7c750eebce2c8cb1dbfc73514bf318ea87.yml +openapi_spec_hash: ffccc75cdb734af5746192289c4fce9b +config_hash: 600d71be044aa28b6759ce7f55a86948 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9787795..7ac5cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 1.16.0 (2026-05-13) + +Full Changelog: [v1.15.0...v1.16.0](https://github.com/DayMoonDevelopment/post-for-me-python/compare/v1.15.0...v1.16.0) + +### Features + +* **api:** api update ([836ea67](https://github.com/DayMoonDevelopment/post-for-me-python/commit/836ea6730f3902793fbd6f2c2658408f14fdd26a)) +* **api:** manual updates ([d04ba06](https://github.com/DayMoonDevelopment/post-for-me-python/commit/d04ba064d3473551fbb9f5fa58f7e0746e4e964a)) +* **internal/types:** support eagerly validating pydantic iterators ([e452557](https://github.com/DayMoonDevelopment/post-for-me-python/commit/e452557c17b3a855f1721b22c31d866b3c820ca7)) +* support setting headers via env ([2aa1883](https://github.com/DayMoonDevelopment/post-for-me-python/commit/2aa18833f588027ae62e86d4d4e9e376cb509405)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([66a4a79](https://github.com/DayMoonDevelopment/post-for-me-python/commit/66a4a79e88650652dcc5fd7653edc5110c5f4590)) +* use correct field name format for multipart file arrays ([8165256](https://github.com/DayMoonDevelopment/post-for-me-python/commit/8165256e26d948d2be4711116ed30004007ba6bf)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([897e06a](https://github.com/DayMoonDevelopment/post-for-me-python/commit/897e06a740ac67aaacd96fc1c70a2cb07407d02a)) + + +### Chores + +* **internal:** more robust bootstrap script ([0c3c4f0](https://github.com/DayMoonDevelopment/post-for-me-python/commit/0c3c4f06ec91632e5b34d0651c058f4b2ad51065)) +* **internal:** reformat pyproject.toml ([a1ee9d9](https://github.com/DayMoonDevelopment/post-for-me-python/commit/a1ee9d96ce4e823150ef165b857ce5e839beea3f)) + ## 1.15.0 (2026-04-11) Full Changelog: [v1.14.0...v1.15.0](https://github.com/DayMoonDevelopment/post-for-me-python/compare/v1.14.0...v1.15.0) diff --git a/api.md b/api.md index 0acf4fa..d14efcb 100644 --- a/api.md +++ b/api.md @@ -16,20 +16,23 @@ Types: ```python from post_for_me.types import ( + AccountConfiguration, BlueskyConfigurationDto, CreateSocialPost, + DeleteEntityResponse, FacebookConfigurationDto, InstagramConfigurationDto, LinkedinConfigurationDto, PinterestConfigurationDto, PlatformConfigurationsDto, SocialPost, + SocialPostMedia, ThreadsConfigurationDto, TiktokConfiguration, TwitterConfigurationDto, + TwitterPoll, YoutubeConfigurationDto, SocialPostListResponse, - SocialPostDeleteResponse, ) ``` @@ -39,7 +42,7 @@ Methods: - client.social_posts.retrieve(id) -> SocialPost - client.social_posts.update(id, \*\*params) -> SocialPost - client.social_posts.list(\*\*params) -> SocialPostListResponse -- client.social_posts.delete(id) -> SocialPostDeleteResponse +- client.social_posts.delete(id) -> DeleteEntityResponse # SocialPostResults @@ -61,6 +64,7 @@ Types: ```python from post_for_me.types import ( SocialAccount, + SocialAccountMetadata, SocialAccountListResponse, SocialAccountCreateAuthURLResponse, SocialAccountDisconnectResponse, @@ -81,9 +85,50 @@ Methods: Types: ```python -from post_for_me.types import PlatformPost, SocialAccountFeedListResponse +from post_for_me.types import ( + FacebookActivityByActionType, + FacebookVideoRetentionGraph, + FacebookVideoViewTimeByDemographic, + PinterestMetricsWindow, + PlatformPost, + TiktokBusinessVideoMetricPercentage, + YoutubePostPlatformData, + SocialAccountFeedListResponse, +) ``` Methods: - client.social_account_feeds.list(social_account_id, \*\*params) -> SocialAccountFeedListResponse + +# Webhooks + +Types: + +```python +from post_for_me.types import Webhook, WebhookListResponse +``` + +Methods: + +- client.webhooks.create(\*\*params) -> Webhook +- client.webhooks.retrieve(id) -> Webhook +- client.webhooks.update(id, \*\*params) -> Webhook +- client.webhooks.list(\*\*params) -> WebhookListResponse +- client.webhooks.delete(id) -> DeleteEntityResponse + +# SocialPostPreviews + +Types: + +```python +from post_for_me.types import ( + CreateSocialPostPreview, + SocialPostPreview, + SocialPostPreviewCreateResponse, +) +``` + +Methods: + +- client.social_post_previews.create(\*\*params) -> SocialPostPreviewCreateResponse diff --git a/pyproject.toml b/pyproject.toml index 7cff57c..281550e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "post_for_me" -version = "1.15.0" +version = "1.16.0" description = "The official Python library for the post-for-me API" dynamic = ["readme"] license = "Apache-2.0" @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/post_for_me/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/post_for_me/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee..fe8451e 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response diff --git a/src/post_for_me/_client.py b/src/post_for_me/_client.py index 31f98be..b9d5271 100644 --- a/src/post_for_me/_client.py +++ b/src/post_for_me/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -31,12 +35,22 @@ ) if TYPE_CHECKING: - from .resources import media, social_posts, social_accounts, social_post_results, social_account_feeds + from .resources import ( + media, + webhooks, + social_posts, + social_accounts, + social_post_results, + social_account_feeds, + social_post_previews, + ) from .resources.media import MediaResource, AsyncMediaResource + from .resources.webhooks import WebhooksResource, AsyncWebhooksResource from .resources.social_posts import SocialPostsResource, AsyncSocialPostsResource from .resources.social_accounts import SocialAccountsResource, AsyncSocialAccountsResource from .resources.social_post_results import SocialPostResultsResource, AsyncSocialPostResultsResource from .resources.social_account_feeds import SocialAccountFeedsResource, AsyncSocialAccountFeedsResource + from .resources.social_post_previews import SocialPostPreviewsResource, AsyncSocialPostPreviewsResource __all__ = [ "Timeout", @@ -94,6 +108,15 @@ def __init__( if base_url is None: base_url = f"https://api.postforme.dev" + custom_headers_env = os.environ.get("POST_FOR_ME_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -182,6 +205,45 @@ def social_account_feeds(self) -> SocialAccountFeedsResource: return SocialAccountFeedsResource(self) + @cached_property + def webhooks(self) -> WebhooksResource: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import WebhooksResource + + return WebhooksResource(self) + + @cached_property + def social_post_previews(self) -> SocialPostPreviewsResource: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import SocialPostPreviewsResource + + return SocialPostPreviewsResource(self) + @cached_property def with_raw_response(self) -> PostForMeWithRawResponse: return PostForMeWithRawResponse(self) @@ -339,6 +401,15 @@ def __init__( if base_url is None: base_url = f"https://api.postforme.dev" + custom_headers_env = os.environ.get("POST_FOR_ME_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -427,6 +498,45 @@ def social_account_feeds(self) -> AsyncSocialAccountFeedsResource: return AsyncSocialAccountFeedsResource(self) + @cached_property + def webhooks(self) -> AsyncWebhooksResource: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import AsyncWebhooksResource + + return AsyncWebhooksResource(self) + + @cached_property + def social_post_previews(self) -> AsyncSocialPostPreviewsResource: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import AsyncSocialPostPreviewsResource + + return AsyncSocialPostPreviewsResource(self) + @cached_property def with_raw_response(self) -> AsyncPostForMeWithRawResponse: return AsyncPostForMeWithRawResponse(self) @@ -623,6 +733,45 @@ def social_account_feeds(self) -> social_account_feeds.SocialAccountFeedsResourc return SocialAccountFeedsResourceWithRawResponse(self._client.social_account_feeds) + @cached_property + def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import WebhooksResourceWithRawResponse + + return WebhooksResourceWithRawResponse(self._client.webhooks) + + @cached_property + def social_post_previews(self) -> social_post_previews.SocialPostPreviewsResourceWithRawResponse: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import SocialPostPreviewsResourceWithRawResponse + + return SocialPostPreviewsResourceWithRawResponse(self._client.social_post_previews) + class AsyncPostForMeWithRawResponse: _client: AsyncPostForMe @@ -707,6 +856,45 @@ def social_account_feeds(self) -> social_account_feeds.AsyncSocialAccountFeedsRe return AsyncSocialAccountFeedsResourceWithRawResponse(self._client.social_account_feeds) + @cached_property + def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import AsyncWebhooksResourceWithRawResponse + + return AsyncWebhooksResourceWithRawResponse(self._client.webhooks) + + @cached_property + def social_post_previews(self) -> social_post_previews.AsyncSocialPostPreviewsResourceWithRawResponse: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import AsyncSocialPostPreviewsResourceWithRawResponse + + return AsyncSocialPostPreviewsResourceWithRawResponse(self._client.social_post_previews) + class PostForMeWithStreamedResponse: _client: PostForMe @@ -791,6 +979,45 @@ def social_account_feeds(self) -> social_account_feeds.SocialAccountFeedsResourc return SocialAccountFeedsResourceWithStreamingResponse(self._client.social_account_feeds) + @cached_property + def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import WebhooksResourceWithStreamingResponse + + return WebhooksResourceWithStreamingResponse(self._client.webhooks) + + @cached_property + def social_post_previews(self) -> social_post_previews.SocialPostPreviewsResourceWithStreamingResponse: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import SocialPostPreviewsResourceWithStreamingResponse + + return SocialPostPreviewsResourceWithStreamingResponse(self._client.social_post_previews) + class AsyncPostForMeWithStreamedResponse: _client: AsyncPostForMe @@ -875,6 +1102,45 @@ def social_account_feeds(self) -> social_account_feeds.AsyncSocialAccountFeedsRe return AsyncSocialAccountFeedsResourceWithStreamingResponse(self._client.social_account_feeds) + @cached_property + def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse: + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse + + return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks) + + @cached_property + def social_post_previews(self) -> social_post_previews.AsyncSocialPostPreviewsResourceWithStreamingResponse: + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + from .resources.social_post_previews import AsyncSocialPostPreviewsResourceWithStreamingResponse + + return AsyncSocialPostPreviewsResourceWithStreamingResponse(self._client.social_post_previews) + Client = PostForMe diff --git a/src/post_for_me/_files.py b/src/post_for_me/_files.py index cc14c14..76da9e0 100644 --- a/src/post_for_me/_files.py +++ b/src/post_for_me/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -97,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/post_for_me/_models.py b/src/post_for_me/_models.py index 29070e0..8c5ab26 100644 --- a/src/post_for_me/_models.py +++ b/src/post_for_me/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/src/post_for_me/_qs.py b/src/post_for_me/_qs.py index de8c99b..4127c19 100644 --- a/src/post_for_me/_qs.py +++ b/src/post_for_me/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/post_for_me/_types.py b/src/post_for_me/_types.py index 0290e44..34f4d44 100644 --- a/src/post_for_me/_types.py +++ b/src/post_for_me/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/post_for_me/_utils/__init__.py b/src/post_for_me/_utils/__init__.py index 10cb66d..1c090e5 100644 --- a/src/post_for_me/_utils/__init__.py +++ b/src/post_for_me/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/post_for_me/_utils/_utils.py b/src/post_for_me/_utils/_utils.py index 63b8cd6..199cd23 100644 --- a/src/post_for_me/_utils/_utils.py +++ b/src/post_for_me/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) @@ -177,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/post_for_me/_version.py b/src/post_for_me/_version.py index 207906a..5b74e50 100644 --- a/src/post_for_me/_version.py +++ b/src/post_for_me/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "post_for_me" -__version__ = "1.15.0" # x-release-please-version +__version__ = "1.16.0" # x-release-please-version diff --git a/src/post_for_me/resources/__init__.py b/src/post_for_me/resources/__init__.py index 46085d5..c99cec3 100644 --- a/src/post_for_me/resources/__init__.py +++ b/src/post_for_me/resources/__init__.py @@ -8,6 +8,14 @@ MediaResourceWithStreamingResponse, AsyncMediaResourceWithStreamingResponse, ) +from .webhooks import ( + WebhooksResource, + AsyncWebhooksResource, + WebhooksResourceWithRawResponse, + AsyncWebhooksResourceWithRawResponse, + WebhooksResourceWithStreamingResponse, + AsyncWebhooksResourceWithStreamingResponse, +) from .social_posts import ( SocialPostsResource, AsyncSocialPostsResource, @@ -40,6 +48,14 @@ SocialAccountFeedsResourceWithStreamingResponse, AsyncSocialAccountFeedsResourceWithStreamingResponse, ) +from .social_post_previews import ( + SocialPostPreviewsResource, + AsyncSocialPostPreviewsResource, + SocialPostPreviewsResourceWithRawResponse, + AsyncSocialPostPreviewsResourceWithRawResponse, + SocialPostPreviewsResourceWithStreamingResponse, + AsyncSocialPostPreviewsResourceWithStreamingResponse, +) __all__ = [ "MediaResource", @@ -72,4 +88,16 @@ "AsyncSocialAccountFeedsResourceWithRawResponse", "SocialAccountFeedsResourceWithStreamingResponse", "AsyncSocialAccountFeedsResourceWithStreamingResponse", + "WebhooksResource", + "AsyncWebhooksResource", + "WebhooksResourceWithRawResponse", + "AsyncWebhooksResourceWithRawResponse", + "WebhooksResourceWithStreamingResponse", + "AsyncWebhooksResourceWithStreamingResponse", + "SocialPostPreviewsResource", + "AsyncSocialPostPreviewsResource", + "SocialPostPreviewsResourceWithRawResponse", + "AsyncSocialPostPreviewsResourceWithRawResponse", + "SocialPostPreviewsResourceWithStreamingResponse", + "AsyncSocialPostPreviewsResourceWithStreamingResponse", ] diff --git a/src/post_for_me/resources/social_accounts.py b/src/post_for_me/resources/social_accounts.py index 923cb40..5208c67 100644 --- a/src/post_for_me/resources/social_accounts.py +++ b/src/post_for_me/resources/social_accounts.py @@ -27,6 +27,7 @@ from .._base_client import make_request_options from ..types.social_account import SocialAccount from ..types.social_account_list_response import SocialAccountListResponse +from ..types.social_account_metadata_param import SocialAccountMetadataParam from ..types.social_account_disconnect_response import SocialAccountDisconnectResponse from ..types.social_account_create_auth_url_response import SocialAccountCreateAuthURLResponse @@ -78,7 +79,7 @@ def create( ], user_id: str, external_id: Optional[str] | Omit = omit, - metadata: object | Omit = omit, + metadata: SocialAccountMetadataParam | Omit = omit, refresh_token: Optional[str] | Omit = omit, refresh_token_expires_at: Union[str, datetime, None] | Omit = omit, username: Optional[str] | Omit = omit, @@ -435,7 +436,7 @@ async def create( ], user_id: str, external_id: Optional[str] | Omit = omit, - metadata: object | Omit = omit, + metadata: SocialAccountMetadataParam | Omit = omit, refresh_token: Optional[str] | Omit = omit, refresh_token_expires_at: Union[str, datetime, None] | Omit = omit, username: Optional[str] | Omit = omit, diff --git a/src/post_for_me/resources/social_post_previews.py b/src/post_for_me/resources/social_post_previews.py new file mode 100644 index 0000000..97b1375 --- /dev/null +++ b/src/post_for_me/resources/social_post_previews.py @@ -0,0 +1,224 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional + +import httpx + +from ..types import social_post_preview_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.social_post_media_param import SocialPostMediaParam +from ..types.account_configuration_param import AccountConfigurationParam +from ..types.platform_configurations_dto_param import PlatformConfigurationsDtoParam +from ..types.social_post_preview_create_response import SocialPostPreviewCreateResponse + +__all__ = ["SocialPostPreviewsResource", "AsyncSocialPostPreviewsResource"] + + +class SocialPostPreviewsResource(SyncAPIResource): + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + + @cached_property + def with_raw_response(self) -> SocialPostPreviewsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#accessing-raw-response-data-eg-headers + """ + return SocialPostPreviewsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SocialPostPreviewsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#with_streaming_response + """ + return SocialPostPreviewsResourceWithStreamingResponse(self) + + def create( + self, + *, + caption: str, + preview_social_accounts: Iterable[social_post_preview_create_params.PreviewSocialAccount], + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, + platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SocialPostPreviewCreateResponse: + """ + Create Post Previews + + Args: + caption: Caption text for the post + + preview_social_accounts: Array of social accounts. Can preview non connected accounts, just specify a + random ID + + account_configurations: Account-specific configurations for the post + + media: Array of media URLs associated with the post + + platform_configurations: Platform-specific configurations for the post + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/social-post-previews", + body=maybe_transform( + { + "caption": caption, + "preview_social_accounts": preview_social_accounts, + "account_configurations": account_configurations, + "media": media, + "platform_configurations": platform_configurations, + }, + social_post_preview_create_params.SocialPostPreviewCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SocialPostPreviewCreateResponse, + ) + + +class AsyncSocialPostPreviewsResource(AsyncAPIResource): + """ + Social Post Previews allow you to see what a Social Post will create for each account in the post. + """ + + @cached_property + def with_raw_response(self) -> AsyncSocialPostPreviewsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#accessing-raw-response-data-eg-headers + """ + return AsyncSocialPostPreviewsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSocialPostPreviewsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#with_streaming_response + """ + return AsyncSocialPostPreviewsResourceWithStreamingResponse(self) + + async def create( + self, + *, + caption: str, + preview_social_accounts: Iterable[social_post_preview_create_params.PreviewSocialAccount], + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, + platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SocialPostPreviewCreateResponse: + """ + Create Post Previews + + Args: + caption: Caption text for the post + + preview_social_accounts: Array of social accounts. Can preview non connected accounts, just specify a + random ID + + account_configurations: Account-specific configurations for the post + + media: Array of media URLs associated with the post + + platform_configurations: Platform-specific configurations for the post + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/social-post-previews", + body=await async_maybe_transform( + { + "caption": caption, + "preview_social_accounts": preview_social_accounts, + "account_configurations": account_configurations, + "media": media, + "platform_configurations": platform_configurations, + }, + social_post_preview_create_params.SocialPostPreviewCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SocialPostPreviewCreateResponse, + ) + + +class SocialPostPreviewsResourceWithRawResponse: + def __init__(self, social_post_previews: SocialPostPreviewsResource) -> None: + self._social_post_previews = social_post_previews + + self.create = to_raw_response_wrapper( + social_post_previews.create, + ) + + +class AsyncSocialPostPreviewsResourceWithRawResponse: + def __init__(self, social_post_previews: AsyncSocialPostPreviewsResource) -> None: + self._social_post_previews = social_post_previews + + self.create = async_to_raw_response_wrapper( + social_post_previews.create, + ) + + +class SocialPostPreviewsResourceWithStreamingResponse: + def __init__(self, social_post_previews: SocialPostPreviewsResource) -> None: + self._social_post_previews = social_post_previews + + self.create = to_streamed_response_wrapper( + social_post_previews.create, + ) + + +class AsyncSocialPostPreviewsResourceWithStreamingResponse: + def __init__(self, social_post_previews: AsyncSocialPostPreviewsResource) -> None: + self._social_post_previews = social_post_previews + + self.create = async_to_streamed_response_wrapper( + social_post_previews.create, + ) diff --git a/src/post_for_me/resources/social_posts.py b/src/post_for_me/resources/social_posts.py index 9b111ce..b737ea7 100644 --- a/src/post_for_me/resources/social_posts.py +++ b/src/post_for_me/resources/social_posts.py @@ -25,8 +25,10 @@ ) from .._base_client import make_request_options from ..types.social_post import SocialPost +from ..types.delete_entity_response import DeleteEntityResponse +from ..types.social_post_media_param import SocialPostMediaParam from ..types.social_post_list_response import SocialPostListResponse -from ..types.social_post_delete_response import SocialPostDeleteResponse +from ..types.account_configuration_param import AccountConfigurationParam from ..types.platform_configurations_dto_param import PlatformConfigurationsDtoParam __all__ = ["SocialPostsResource", "AsyncSocialPostsResource"] @@ -67,10 +69,10 @@ def create( *, caption: str, social_accounts: SequenceNotStr[str], - account_configurations: Optional[Iterable[social_post_create_params.AccountConfiguration]] | Omit = omit, + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, external_id: Optional[str] | Omit = omit, is_draft: Optional[bool] | Omit = omit, - media: Optional[Iterable[social_post_create_params.Media]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, scheduled_at: Union[str, datetime, None] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -170,10 +172,10 @@ def update( *, caption: str, social_accounts: SequenceNotStr[str], - account_configurations: Optional[Iterable[social_post_update_params.AccountConfiguration]] | Omit = omit, + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, external_id: Optional[str] | Omit = omit, is_draft: Optional[bool] | Omit = omit, - media: Optional[Iterable[social_post_update_params.Media]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, scheduled_at: Union[str, datetime, None] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -311,7 +313,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SocialPostDeleteResponse: + ) -> DeleteEntityResponse: """ Delete Post @@ -331,7 +333,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SocialPostDeleteResponse, + cast_to=DeleteEntityResponse, ) @@ -370,10 +372,10 @@ async def create( *, caption: str, social_accounts: SequenceNotStr[str], - account_configurations: Optional[Iterable[social_post_create_params.AccountConfiguration]] | Omit = omit, + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, external_id: Optional[str] | Omit = omit, is_draft: Optional[bool] | Omit = omit, - media: Optional[Iterable[social_post_create_params.Media]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, scheduled_at: Union[str, datetime, None] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -473,10 +475,10 @@ async def update( *, caption: str, social_accounts: SequenceNotStr[str], - account_configurations: Optional[Iterable[social_post_update_params.AccountConfiguration]] | Omit = omit, + account_configurations: Optional[Iterable[AccountConfigurationParam]] | Omit = omit, external_id: Optional[str] | Omit = omit, is_draft: Optional[bool] | Omit = omit, - media: Optional[Iterable[social_post_update_params.Media]] | Omit = omit, + media: Optional[Iterable[SocialPostMediaParam]] | Omit = omit, platform_configurations: Optional[PlatformConfigurationsDtoParam] | Omit = omit, scheduled_at: Union[str, datetime, None] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -614,7 +616,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> SocialPostDeleteResponse: + ) -> DeleteEntityResponse: """ Delete Post @@ -634,7 +636,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=SocialPostDeleteResponse, + cast_to=DeleteEntityResponse, ) diff --git a/src/post_for_me/resources/webhooks.py b/src/post_for_me/resources/webhooks.py new file mode 100644 index 0000000..43c4ee6 --- /dev/null +++ b/src/post_for_me/resources/webhooks.py @@ -0,0 +1,672 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal + +import httpx + +from ..types import webhook_list_params, webhook_create_params, webhook_update_params +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.webhook import Webhook +from ..types.webhook_list_response import WebhookListResponse +from ..types.delete_entity_response import DeleteEntityResponse + +__all__ = ["WebhooksResource", "AsyncWebhooksResource"] + + +class WebhooksResource(SyncAPIResource): + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + + @cached_property + def with_raw_response(self) -> WebhooksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#accessing-raw-response-data-eg-headers + """ + return WebhooksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#with_streaming_response + """ + return WebhooksResourceWithStreamingResponse(self) + + def create( + self, + *, + event_types: List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ], + url: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Create Webhook + + Args: + event_types: List of events the webhook will recieve + + url: Public url to recieve event data + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/webhooks", + body=maybe_transform( + { + "event_types": event_types, + "url": url, + }, + webhook_create_params.WebhookCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Get webhook by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + path_template("/v1/webhooks/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + def update( + self, + id: str, + *, + event_types: List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ] + | Omit = omit, + url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Update Webhook + + Args: + event_types: List of events the webhook will recieve + + url: Public url to recieve event data + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + path_template("/v1/webhooks/{id}", id=id), + body=maybe_transform( + { + "event_types": event_types, + "url": url, + }, + webhook_update_params.WebhookUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + def list( + self, + *, + id: SequenceNotStr[str] | Omit = omit, + event_type: SequenceNotStr[str] | Omit = omit, + limit: float | Omit = omit, + offset: float | Omit = omit, + url: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookListResponse: + """ + Get a paginated result for webhooks based on the applied filters + + Args: + id: Filter by id(s). Multiple values imply OR logic (e.g., + ?id=wbh_xxxxxx&id=wbh_yyyyyy). + + event_type: Filter by event type(s). Multiple values imply OR logic (e.g., + ?event_type=social.post.created&event_type=social.post.updated). + + limit: Number of items to return + + offset: Number of items to skip + + url: Filter by url(s). Multiple values imply OR logic (e.g., + ?url=https://example.com&url=https://postforme.dev). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v1/webhooks", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "id": id, + "event_type": event_type, + "limit": limit, + "offset": offset, + "url": url, + }, + webhook_list_params.WebhookListParams, + ), + ), + cast_to=WebhookListResponse, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DeleteEntityResponse: + """ + Delete Webhook + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._delete( + path_template("/v1/webhooks/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeleteEntityResponse, + ) + + +class AsyncWebhooksResource(AsyncAPIResource): + """Webhooks enable you to subscribe to certain events. + + This involves Post for Me making a POST request to the URL of any webhooks you create. + Only the events you subscribe to will be sent to your webhook URL. + + ## Payload + When an event happens that your webhook is subscribed to, we will make a POST request with the following JSON body + + ``` + { + "event_type": "", + "data": {} + } + ``` + + The event_type will be the event that triggered the webhook POST, data will be the resulting entity from the event + + ## Security + To verify the POST to your webhook URL is from us we will include a secret in the header "Post-For-Me-Webhook-Secret". + When you create a webhook you will receive the secret in the response. + + ## Retries + If your server fails to respond with a 2XX code, requests to it will be retried with exponential backoff around 8 times over the course of just over a day. + """ + + @cached_property + def with_raw_response(self) -> AsyncWebhooksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#accessing-raw-response-data-eg-headers + """ + return AsyncWebhooksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/DayMoonDevelopment/post-for-me-python#with_streaming_response + """ + return AsyncWebhooksResourceWithStreamingResponse(self) + + async def create( + self, + *, + event_types: List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ], + url: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Create Webhook + + Args: + event_types: List of events the webhook will recieve + + url: Public url to recieve event data + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/webhooks", + body=await async_maybe_transform( + { + "event_types": event_types, + "url": url, + }, + webhook_create_params.WebhookCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Get webhook by ID + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + path_template("/v1/webhooks/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + async def update( + self, + id: str, + *, + event_types: List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ] + | Omit = omit, + url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Update Webhook + + Args: + event_types: List of events the webhook will recieve + + url: Public url to recieve event data + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + path_template("/v1/webhooks/{id}", id=id), + body=await async_maybe_transform( + { + "event_types": event_types, + "url": url, + }, + webhook_update_params.WebhookUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + async def list( + self, + *, + id: SequenceNotStr[str] | Omit = omit, + event_type: SequenceNotStr[str] | Omit = omit, + limit: float | Omit = omit, + offset: float | Omit = omit, + url: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookListResponse: + """ + Get a paginated result for webhooks based on the applied filters + + Args: + id: Filter by id(s). Multiple values imply OR logic (e.g., + ?id=wbh_xxxxxx&id=wbh_yyyyyy). + + event_type: Filter by event type(s). Multiple values imply OR logic (e.g., + ?event_type=social.post.created&event_type=social.post.updated). + + limit: Number of items to return + + offset: Number of items to skip + + url: Filter by url(s). Multiple values imply OR logic (e.g., + ?url=https://example.com&url=https://postforme.dev). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/v1/webhooks", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "id": id, + "event_type": event_type, + "limit": limit, + "offset": offset, + "url": url, + }, + webhook_list_params.WebhookListParams, + ), + ), + cast_to=WebhookListResponse, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DeleteEntityResponse: + """ + Delete Webhook + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._delete( + path_template("/v1/webhooks/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeleteEntityResponse, + ) + + +class WebhooksResourceWithRawResponse: + def __init__(self, webhooks: WebhooksResource) -> None: + self._webhooks = webhooks + + self.create = to_raw_response_wrapper( + webhooks.create, + ) + self.retrieve = to_raw_response_wrapper( + webhooks.retrieve, + ) + self.update = to_raw_response_wrapper( + webhooks.update, + ) + self.list = to_raw_response_wrapper( + webhooks.list, + ) + self.delete = to_raw_response_wrapper( + webhooks.delete, + ) + + +class AsyncWebhooksResourceWithRawResponse: + def __init__(self, webhooks: AsyncWebhooksResource) -> None: + self._webhooks = webhooks + + self.create = async_to_raw_response_wrapper( + webhooks.create, + ) + self.retrieve = async_to_raw_response_wrapper( + webhooks.retrieve, + ) + self.update = async_to_raw_response_wrapper( + webhooks.update, + ) + self.list = async_to_raw_response_wrapper( + webhooks.list, + ) + self.delete = async_to_raw_response_wrapper( + webhooks.delete, + ) + + +class WebhooksResourceWithStreamingResponse: + def __init__(self, webhooks: WebhooksResource) -> None: + self._webhooks = webhooks + + self.create = to_streamed_response_wrapper( + webhooks.create, + ) + self.retrieve = to_streamed_response_wrapper( + webhooks.retrieve, + ) + self.update = to_streamed_response_wrapper( + webhooks.update, + ) + self.list = to_streamed_response_wrapper( + webhooks.list, + ) + self.delete = to_streamed_response_wrapper( + webhooks.delete, + ) + + +class AsyncWebhooksResourceWithStreamingResponse: + def __init__(self, webhooks: AsyncWebhooksResource) -> None: + self._webhooks = webhooks + + self.create = async_to_streamed_response_wrapper( + webhooks.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + webhooks.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + webhooks.update, + ) + self.list = async_to_streamed_response_wrapper( + webhooks.list, + ) + self.delete = async_to_streamed_response_wrapper( + webhooks.delete, + ) diff --git a/src/post_for_me/types/__init__.py b/src/post_for_me/types/__init__.py index 5987569..062d8a6 100644 --- a/src/post_for_me/types/__init__.py +++ b/src/post_for_me/types/__init__.py @@ -2,12 +2,25 @@ from __future__ import annotations +from .webhook import Webhook as Webhook from .social_post import SocialPost as SocialPost +from .twitter_poll import TwitterPoll as TwitterPoll from .platform_post import PlatformPost as PlatformPost from .social_account import SocialAccount as SocialAccount +from .social_post_media import SocialPostMedia as SocialPostMedia from .social_post_result import SocialPostResult as SocialPostResult +from .twitter_poll_param import TwitterPollParam as TwitterPollParam +from .social_post_preview import SocialPostPreview as SocialPostPreview +from .webhook_list_params import WebhookListParams as WebhookListParams from .tiktok_configuration import TiktokConfiguration as TiktokConfiguration +from .account_configuration import AccountConfiguration as AccountConfiguration +from .webhook_create_params import WebhookCreateParams as WebhookCreateParams +from .webhook_list_response import WebhookListResponse as WebhookListResponse +from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams +from .delete_entity_response import DeleteEntityResponse as DeleteEntityResponse from .social_post_list_params import SocialPostListParams as SocialPostListParams +from .social_post_media_param import SocialPostMediaParam as SocialPostMediaParam +from .pinterest_metrics_window import PinterestMetricsWindow as PinterestMetricsWindow from .bluesky_configuration_dto import BlueskyConfigurationDto as BlueskyConfigurationDto from .social_post_create_params import SocialPostCreateParams as SocialPostCreateParams from .social_post_list_response import SocialPostListResponse as SocialPostListResponse @@ -19,19 +32,23 @@ from .linkedin_configuration_dto import LinkedinConfigurationDto as LinkedinConfigurationDto from .social_account_list_params import SocialAccountListParams as SocialAccountListParams from .tiktok_configuration_param import TiktokConfigurationParam as TiktokConfigurationParam +from .youtube_post_platform_data import YoutubePostPlatformData as YoutubePostPlatformData +from .account_configuration_param import AccountConfigurationParam as AccountConfigurationParam from .instagram_configuration_dto import InstagramConfigurationDto as InstagramConfigurationDto from .pinterest_configuration_dto import PinterestConfigurationDto as PinterestConfigurationDto from .platform_configurations_dto import PlatformConfigurationsDto as PlatformConfigurationsDto -from .social_post_delete_response import SocialPostDeleteResponse as SocialPostDeleteResponse from .social_account_create_params import SocialAccountCreateParams as SocialAccountCreateParams from .social_account_list_response import SocialAccountListResponse as SocialAccountListResponse from .social_account_update_params import SocialAccountUpdateParams as SocialAccountUpdateParams +from .social_account_metadata_param import SocialAccountMetadataParam as SocialAccountMetadataParam +from .facebook_video_retention_graph import FacebookVideoRetentionGraph as FacebookVideoRetentionGraph from .social_post_result_list_params import SocialPostResultListParams as SocialPostResultListParams from .bluesky_configuration_dto_param import BlueskyConfigurationDtoParam as BlueskyConfigurationDtoParam from .social_account_feed_list_params import SocialAccountFeedListParams as SocialAccountFeedListParams from .threads_configuration_dto_param import ThreadsConfigurationDtoParam as ThreadsConfigurationDtoParam from .twitter_configuration_dto_param import TwitterConfigurationDtoParam as TwitterConfigurationDtoParam from .youtube_configuration_dto_param import YoutubeConfigurationDtoParam as YoutubeConfigurationDtoParam +from .facebook_activity_by_action_type import FacebookActivityByActionType as FacebookActivityByActionType from .facebook_configuration_dto_param import FacebookConfigurationDtoParam as FacebookConfigurationDtoParam from .linkedin_configuration_dto_param import LinkedinConfigurationDtoParam as LinkedinConfigurationDtoParam from .media_create_upload_url_response import MediaCreateUploadURLResponse as MediaCreateUploadURLResponse @@ -40,8 +57,16 @@ from .pinterest_configuration_dto_param import PinterestConfigurationDtoParam as PinterestConfigurationDtoParam from .platform_configurations_dto_param import PlatformConfigurationsDtoParam as PlatformConfigurationsDtoParam from .social_account_feed_list_response import SocialAccountFeedListResponse as SocialAccountFeedListResponse +from .social_post_preview_create_params import SocialPostPreviewCreateParams as SocialPostPreviewCreateParams from .social_account_disconnect_response import SocialAccountDisconnectResponse as SocialAccountDisconnectResponse +from .social_post_preview_create_response import SocialPostPreviewCreateResponse as SocialPostPreviewCreateResponse from .social_account_create_auth_url_params import SocialAccountCreateAuthURLParams as SocialAccountCreateAuthURLParams +from .facebook_video_view_time_by_demographic import ( + FacebookVideoViewTimeByDemographic as FacebookVideoViewTimeByDemographic, +) from .social_account_create_auth_url_response import ( SocialAccountCreateAuthURLResponse as SocialAccountCreateAuthURLResponse, ) +from .tiktok_business_video_metric_percentage import ( + TiktokBusinessVideoMetricPercentage as TiktokBusinessVideoMetricPercentage, +) diff --git a/src/post_for_me/types/account_configuration.py b/src/post_for_me/types/account_configuration.py new file mode 100644 index 0000000..c45866f --- /dev/null +++ b/src/post_for_me/types/account_configuration.py @@ -0,0 +1,117 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .twitter_poll import TwitterPoll +from .social_post_media import SocialPostMedia + +__all__ = ["AccountConfiguration", "Configuration"] + + +class Configuration(BaseModel): + """Configuration for the social account""" + + allow_comment: Optional[bool] = None + """Allow comments on TikTok""" + + allow_duet: Optional[bool] = None + """Allow duets on TikTok""" + + allow_stitch: Optional[bool] = None + """Allow stitch on TikTok""" + + auto_add_music: Optional[bool] = None + """Will automatically add music to photo posts on TikTok""" + + board_ids: Optional[List[str]] = None + """Pinterest board IDs""" + + caption: Optional[object] = None + """Overrides the `caption` from the post""" + + collaborators: Optional[List[List[object]]] = None + """ + List of page ids or users to invite as collaborators for a Video Reel (Instagram + and Facebook) + """ + + community_id: Optional[str] = None + """Id of the twitter community to post to""" + + disclose_branded_content: Optional[bool] = None + """Disclose branded content on TikTok""" + + disclose_your_brand: Optional[bool] = None + """Disclose your brand on TikTok""" + + is_ai_generated: Optional[bool] = None + """Flag content as AI generated on TikTok""" + + is_draft: Optional[bool] = None + """ + Will create a draft upload to TikTok, posting will need to be completed from + within the app + """ + + link: Optional[str] = None + """Pinterest post link""" + + location: Optional[str] = None + """ + Page id with a location that you want to tag the image or video with (Instagram + and Facebook) + """ + + made_for_kids: Optional[bool] = None + """If true will notify YouTube the video is intended for kids, defaults to false""" + + media: Optional[List[SocialPostMedia]] = None + """Overrides the `media` from the post""" + + placement: Optional[Literal["reels", "timeline", "stories"]] = None + """Post placement for Facebook/Instagram/Threads""" + + poll: Optional[TwitterPoll] = None + """Poll options for the twitter""" + + privacy_status: Optional[Literal["public", "private", "unlisted"]] = None + """ + Sets the privacy status for TikTok (private, public), or YouTube (private, + public, unlisted) + """ + + quote_tweet_id: Optional[str] = None + """Id of the tweet you want to quote""" + + reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] = None + """Who can reply to the tweet""" + + set_caption_for_each_image: Optional[bool] = None + """ + If true, include the caption on each image in a Facebook carousel upload; if + false, only include it on the final carousel post + """ + + share_to_feed: Optional[bool] = None + """If false Instagram video posts will only be shown in the Reels tab""" + + title: Optional[str] = None + """Overrides the `title` from the post (Pinterest, TikTok, YouTube)""" + + trial_reel_type: Optional[Literal["manual", "performance"]] = None + """Instagram trial reel type, when passed will be created as a trial reel. + + If manual the trial reel can be manually graduated in the native app. If + perfomance the trial reel will be automatically graduated if the trial reel + performs well. + """ + + +class AccountConfiguration(BaseModel): + configuration: Configuration + """Configuration for the social account""" + + social_account_id: str + """ID of the social account, you want to apply the configuration to""" diff --git a/src/post_for_me/types/account_configuration_param.py b/src/post_for_me/types/account_configuration_param.py new file mode 100644 index 0000000..0966289 --- /dev/null +++ b/src/post_for_me/types/account_configuration_param.py @@ -0,0 +1,119 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr +from .twitter_poll_param import TwitterPollParam +from .social_post_media_param import SocialPostMediaParam + +__all__ = ["AccountConfigurationParam", "Configuration"] + + +class Configuration(TypedDict, total=False): + """Configuration for the social account""" + + allow_comment: Optional[bool] + """Allow comments on TikTok""" + + allow_duet: Optional[bool] + """Allow duets on TikTok""" + + allow_stitch: Optional[bool] + """Allow stitch on TikTok""" + + auto_add_music: Optional[bool] + """Will automatically add music to photo posts on TikTok""" + + board_ids: Optional[SequenceNotStr[str]] + """Pinterest board IDs""" + + caption: Optional[object] + """Overrides the `caption` from the post""" + + collaborators: Optional[Iterable[Iterable[object]]] + """ + List of page ids or users to invite as collaborators for a Video Reel (Instagram + and Facebook) + """ + + community_id: str + """Id of the twitter community to post to""" + + disclose_branded_content: Optional[bool] + """Disclose branded content on TikTok""" + + disclose_your_brand: Optional[bool] + """Disclose your brand on TikTok""" + + is_ai_generated: Optional[bool] + """Flag content as AI generated on TikTok""" + + is_draft: Optional[bool] + """ + Will create a draft upload to TikTok, posting will need to be completed from + within the app + """ + + link: Optional[str] + """Pinterest post link""" + + location: Optional[str] + """ + Page id with a location that you want to tag the image or video with (Instagram + and Facebook) + """ + + made_for_kids: Optional[bool] + """If true will notify YouTube the video is intended for kids, defaults to false""" + + media: Optional[Iterable[SocialPostMediaParam]] + """Overrides the `media` from the post""" + + placement: Optional[Literal["reels", "timeline", "stories"]] + """Post placement for Facebook/Instagram/Threads""" + + poll: TwitterPollParam + """Poll options for the twitter""" + + privacy_status: Optional[Literal["public", "private", "unlisted"]] + """ + Sets the privacy status for TikTok (private, public), or YouTube (private, + public, unlisted) + """ + + quote_tweet_id: str + """Id of the tweet you want to quote""" + + reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] + """Who can reply to the tweet""" + + set_caption_for_each_image: Optional[bool] + """ + If true, include the caption on each image in a Facebook carousel upload; if + false, only include it on the final carousel post + """ + + share_to_feed: Optional[bool] + """If false Instagram video posts will only be shown in the Reels tab""" + + title: Optional[str] + """Overrides the `title` from the post (Pinterest, TikTok, YouTube)""" + + trial_reel_type: Optional[Literal["manual", "performance"]] + """Instagram trial reel type, when passed will be created as a trial reel. + + If manual the trial reel can be manually graduated in the native app. If + perfomance the trial reel will be automatically graduated if the trial reel + performs well. + """ + + +class AccountConfigurationParam(TypedDict, total=False): + configuration: Required[Configuration] + """Configuration for the social account""" + + social_account_id: Required[str] + """ID of the social account, you want to apply the configuration to""" diff --git a/src/post_for_me/types/bluesky_configuration_dto.py b/src/post_for_me/types/bluesky_configuration_dto.py index d6141b2..99bdee9 100644 --- a/src/post_for_me/types/bluesky_configuration_dto.py +++ b/src/post_for_me/types/bluesky_configuration_dto.py @@ -1,63 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["BlueskyConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["BlueskyConfigurationDto"] class BlueskyConfigurationDto(BaseModel): caption: Optional[object] = None """Overrides the `caption` from the post""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" diff --git a/src/post_for_me/types/bluesky_configuration_dto_param.py b/src/post_for_me/types/bluesky_configuration_dto_param.py index eff64a4..5d37bd0 100644 --- a/src/post_for_me/types/bluesky_configuration_dto_param.py +++ b/src/post_for_me/types/bluesky_configuration_dto_param.py @@ -3,61 +3,16 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import TypedDict -__all__ = ["BlueskyConfigurationDtoParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["BlueskyConfigurationDtoParam"] class BlueskyConfigurationDtoParam(TypedDict, total=False): caption: Optional[object] """Overrides the `caption` from the post""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" diff --git a/src/post_for_me/types/social_post_delete_response.py b/src/post_for_me/types/delete_entity_response.py similarity index 69% rename from src/post_for_me/types/social_post_delete_response.py rename to src/post_for_me/types/delete_entity_response.py index 4983306..374afa1 100644 --- a/src/post_for_me/types/social_post_delete_response.py +++ b/src/post_for_me/types/delete_entity_response.py @@ -2,9 +2,9 @@ from .._models import BaseModel -__all__ = ["SocialPostDeleteResponse"] +__all__ = ["DeleteEntityResponse"] -class SocialPostDeleteResponse(BaseModel): +class DeleteEntityResponse(BaseModel): success: bool """Whether or not the entity was deleted""" diff --git a/src/post_for_me/types/facebook_activity_by_action_type.py b/src/post_for_me/types/facebook_activity_by_action_type.py new file mode 100644 index 0000000..85aba5b --- /dev/null +++ b/src/post_for_me/types/facebook_activity_by_action_type.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["FacebookActivityByActionType"] + + +class FacebookActivityByActionType(BaseModel): + action_type: str + """Action type (e.g., like, comment, share)""" + + value: float + """Number of actions""" diff --git a/src/post_for_me/types/facebook_configuration_dto.py b/src/post_for_me/types/facebook_configuration_dto.py index 246b33e..74a0a0d 100644 --- a/src/post_for_me/types/facebook_configuration_dto.py +++ b/src/post_for_me/types/facebook_configuration_dto.py @@ -4,55 +4,9 @@ from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["FacebookConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["FacebookConfigurationDto"] class FacebookConfigurationDto(BaseModel): @@ -65,7 +19,7 @@ class FacebookConfigurationDto(BaseModel): location: Optional[str] = None """Page id with a location that you want to tag the image or video with""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" placement: Optional[Literal["reels", "stories", "timeline"]] = None diff --git a/src/post_for_me/types/facebook_configuration_dto_param.py b/src/post_for_me/types/facebook_configuration_dto_param.py index a19274a..4e0cc19 100644 --- a/src/post_for_me/types/facebook_configuration_dto_param.py +++ b/src/post_for_me/types/facebook_configuration_dto_param.py @@ -3,56 +3,11 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict -__all__ = ["FacebookConfigurationDtoParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["FacebookConfigurationDtoParam"] class FacebookConfigurationDtoParam(TypedDict, total=False): @@ -65,7 +20,7 @@ class FacebookConfigurationDtoParam(TypedDict, total=False): location: Optional[str] """Page id with a location that you want to tag the image or video with""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" placement: Optional[Literal["reels", "stories", "timeline"]] diff --git a/src/post_for_me/types/facebook_video_retention_graph.py b/src/post_for_me/types/facebook_video_retention_graph.py new file mode 100644 index 0000000..44b9b46 --- /dev/null +++ b/src/post_for_me/types/facebook_video_retention_graph.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["FacebookVideoRetentionGraph"] + + +class FacebookVideoRetentionGraph(BaseModel): + rate: float + """Percentage of viewers at this time""" + + time: float + """Time in seconds""" diff --git a/src/post_for_me/types/facebook_video_view_time_by_demographic.py b/src/post_for_me/types/facebook_video_view_time_by_demographic.py new file mode 100644 index 0000000..ca9fb67 --- /dev/null +++ b/src/post_for_me/types/facebook_video_view_time_by_demographic.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["FacebookVideoViewTimeByDemographic"] + + +class FacebookVideoViewTimeByDemographic(BaseModel): + key: str + """Demographic key (e.g., age_gender, region, country)""" + + value: float + """Total view time in milliseconds""" diff --git a/src/post_for_me/types/instagram_configuration_dto.py b/src/post_for_me/types/instagram_configuration_dto.py index cde14e8..2f361da 100644 --- a/src/post_for_me/types/instagram_configuration_dto.py +++ b/src/post_for_me/types/instagram_configuration_dto.py @@ -4,55 +4,9 @@ from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["InstagramConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["InstagramConfigurationDto"] class InstagramConfigurationDto(BaseModel): @@ -65,7 +19,7 @@ class InstagramConfigurationDto(BaseModel): location: Optional[str] = None """Page id with a location that you want to tag the image or video with""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" placement: Optional[Literal["reels", "stories", "timeline"]] = None diff --git a/src/post_for_me/types/instagram_configuration_dto_param.py b/src/post_for_me/types/instagram_configuration_dto_param.py index cbdf9c7..fb22a45 100644 --- a/src/post_for_me/types/instagram_configuration_dto_param.py +++ b/src/post_for_me/types/instagram_configuration_dto_param.py @@ -3,58 +3,12 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict from .._types import SequenceNotStr +from .social_post_media_param import SocialPostMediaParam -__all__ = ["InstagramConfigurationDtoParam", "Media", "MediaTag"] - - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["InstagramConfigurationDtoParam"] class InstagramConfigurationDtoParam(TypedDict, total=False): @@ -67,7 +21,7 @@ class InstagramConfigurationDtoParam(TypedDict, total=False): location: Optional[str] """Page id with a location that you want to tag the image or video with""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" placement: Optional[Literal["reels", "stories", "timeline"]] diff --git a/src/post_for_me/types/linkedin_configuration_dto.py b/src/post_for_me/types/linkedin_configuration_dto.py index 4b9e4da..e47ea0f 100644 --- a/src/post_for_me/types/linkedin_configuration_dto.py +++ b/src/post_for_me/types/linkedin_configuration_dto.py @@ -1,63 +1,16 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["LinkedinConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["LinkedinConfigurationDto"] class LinkedinConfigurationDto(BaseModel): caption: Optional[object] = None """Overrides the `caption` from the post""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" diff --git a/src/post_for_me/types/linkedin_configuration_dto_param.py b/src/post_for_me/types/linkedin_configuration_dto_param.py index 2ead64d..d28230e 100644 --- a/src/post_for_me/types/linkedin_configuration_dto_param.py +++ b/src/post_for_me/types/linkedin_configuration_dto_param.py @@ -3,61 +3,16 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import TypedDict -__all__ = ["LinkedinConfigurationDtoParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["LinkedinConfigurationDtoParam"] class LinkedinConfigurationDtoParam(TypedDict, total=False): caption: Optional[object] """Overrides the `caption` from the post""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" diff --git a/src/post_for_me/types/pinterest_configuration_dto.py b/src/post_for_me/types/pinterest_configuration_dto.py index ba6f082..67e0be5 100644 --- a/src/post_for_me/types/pinterest_configuration_dto.py +++ b/src/post_for_me/types/pinterest_configuration_dto.py @@ -1,58 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["PinterestConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["PinterestConfigurationDto"] class PinterestConfigurationDto(BaseModel): @@ -65,5 +18,8 @@ class PinterestConfigurationDto(BaseModel): link: Optional[str] = None """Pinterest post link""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" + + title: Optional[str] = None + """Overrides the `title` from the post for Pinterest""" diff --git a/src/post_for_me/types/pinterest_configuration_dto_param.py b/src/post_for_me/types/pinterest_configuration_dto_param.py index 60845c8..e192080 100644 --- a/src/post_for_me/types/pinterest_configuration_dto_param.py +++ b/src/post_for_me/types/pinterest_configuration_dto_param.py @@ -3,58 +3,12 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import TypedDict from .._types import SequenceNotStr +from .social_post_media_param import SocialPostMediaParam -__all__ = ["PinterestConfigurationDtoParam", "Media", "MediaTag"] - - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["PinterestConfigurationDtoParam"] class PinterestConfigurationDtoParam(TypedDict, total=False): @@ -67,5 +21,8 @@ class PinterestConfigurationDtoParam(TypedDict, total=False): link: Optional[str] """Pinterest post link""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" + + title: Optional[str] + """Overrides the `title` from the post for Pinterest""" diff --git a/src/post_for_me/types/pinterest_metrics_window.py b/src/post_for_me/types/pinterest_metrics_window.py new file mode 100644 index 0000000..04728d8 --- /dev/null +++ b/src/post_for_me/types/pinterest_metrics_window.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["PinterestMetricsWindow"] + + +class PinterestMetricsWindow(BaseModel): + comment: Optional[float] = None + """Number of comments on the Pin""" + + impression: Optional[float] = None + """Number of times the Pin was shown (impressions)""" + + last_updated: Optional[str] = None + """The last time Pinterest updated these metrics""" + + outbound_click: Optional[float] = None + """Number of clicks from the Pin to an external destination (outbound clicks)""" + + pin_click: Optional[float] = None + """Number of clicks on the Pin to view it in closeup (Pin clicks)""" + + profile_visit: Optional[object] = None + """Number of visits to the author's profile driven from the Pin""" + + reaction: Optional[float] = None + """Total number of reactions on the Pin""" + + save: Optional[float] = None + """Number of saves of the Pin""" + + user_follow: Optional[object] = None + """Number of follows driven from the Pin""" + + video_10s_views: Optional[float] = None + """Number of video views of at least 10 seconds""" + + video_average_time: Optional[float] = None + """Average watch time for the video""" + + video_p95_views: Optional[float] = None + """Number of video views that reached 95% completion""" + + video_total_time: Optional[float] = None + """Total watch time for the video""" + + video_views: Optional[float] = None + """Number of video views""" diff --git a/src/post_for_me/types/platform_post.py b/src/post_for_me/types/platform_post.py index 6299598..f5ebe53 100644 --- a/src/post_for_me/types/platform_post.py +++ b/src/post_for_me/types/platform_post.py @@ -7,6 +7,12 @@ from pydantic import Field as FieldInfo from .._models import BaseModel +from .pinterest_metrics_window import PinterestMetricsWindow +from .youtube_post_platform_data import YoutubePostPlatformData +from .facebook_video_retention_graph import FacebookVideoRetentionGraph +from .facebook_activity_by_action_type import FacebookActivityByActionType +from .facebook_video_view_time_by_demographic import FacebookVideoViewTimeByDemographic +from .tiktok_business_video_metric_percentage import TiktokBusinessVideoMetricPercentage __all__ = [ "PlatformPost", @@ -16,20 +22,11 @@ "MetricsTikTokBusinessMetricsDtoAudienceCountry", "MetricsTikTokBusinessMetricsDtoAudienceGender", "MetricsTikTokBusinessMetricsDtoAudienceType", - "MetricsTikTokBusinessMetricsDtoEngagementLike", "MetricsTikTokBusinessMetricsDtoImpressionSource", - "MetricsTikTokBusinessMetricsDtoVideoViewRetention", "MetricsTikTokPostMetricsDto", "MetricsInstagramPostMetricsDto", "MetricsYouTubePostMetricsDto", "MetricsFacebookPostMetricsDto", - "MetricsFacebookPostMetricsDtoActivityByActionType", - "MetricsFacebookPostMetricsDtoActivityByActionTypeUnique", - "MetricsFacebookPostMetricsDtoVideoRetentionGraphAutoplayed", - "MetricsFacebookPostMetricsDtoVideoRetentionGraphClickedToPlay", - "MetricsFacebookPostMetricsDtoVideoViewTimeByAgeGender", - "MetricsFacebookPostMetricsDtoVideoViewTimeByCountry", - "MetricsFacebookPostMetricsDtoVideoViewTimeByRegion", "MetricsTwitterPostMetricsDto", "MetricsTwitterPostMetricsDtoNonPublicMetrics", "MetricsTwitterPostMetricsDtoOrganicMetrics", @@ -38,9 +35,6 @@ "MetricsLinkedInPostMetricsDto", "MetricsBlueskyPostMetricsDto", "MetricsPinterestPostMetricsDto", - "MetricsPinterestPostMetricsDto_90d", - "MetricsPinterestPostMetricsDtoLifetimeMetrics", - "PlatformData", ] @@ -76,14 +70,6 @@ class MetricsTikTokBusinessMetricsDtoAudienceType(BaseModel): """Type of audience""" -class MetricsTikTokBusinessMetricsDtoEngagementLike(BaseModel): - percentage: float - """Percentage value for the metric""" - - second: str - """Time in seconds for the metric""" - - class MetricsTikTokBusinessMetricsDtoImpressionSource(BaseModel): impression_source: str """Name of the impression source""" @@ -92,14 +78,6 @@ class MetricsTikTokBusinessMetricsDtoImpressionSource(BaseModel): """Percentage of impressions from this source""" -class MetricsTikTokBusinessMetricsDtoVideoViewRetention(BaseModel): - percentage: float - """Percentage value for the metric""" - - second: str - """Time in seconds for the metric""" - - class MetricsTikTokBusinessMetricsDto(BaseModel): address_clicks: float """Number of address clicks""" @@ -128,7 +106,7 @@ class MetricsTikTokBusinessMetricsDto(BaseModel): email_clicks: float """Number of email clicks""" - engagement_likes: List[MetricsTikTokBusinessMetricsDtoEngagementLike] + engagement_likes: List[TiktokBusinessVideoMetricPercentage] """Engagement likes data by percentage and time""" favorites: float @@ -164,7 +142,7 @@ class MetricsTikTokBusinessMetricsDto(BaseModel): total_time_watched: float """Total time watched in seconds""" - video_view_retention: List[MetricsTikTokBusinessMetricsDtoVideoViewRetention] + video_view_retention: List[TiktokBusinessVideoMetricPercentage] """Video view retention data by percentage and time""" video_views: float @@ -318,67 +296,11 @@ class MetricsYouTubePostMetricsDto(BaseModel): """Number of times the video was removed from playlists""" -class MetricsFacebookPostMetricsDtoActivityByActionType(BaseModel): - action_type: str - """Action type (e.g., like, comment, share)""" - - value: float - """Number of actions""" - - -class MetricsFacebookPostMetricsDtoActivityByActionTypeUnique(BaseModel): - action_type: str - """Action type (e.g., like, comment, share)""" - - value: float - """Number of actions""" - - -class MetricsFacebookPostMetricsDtoVideoRetentionGraphAutoplayed(BaseModel): - rate: float - """Percentage of viewers at this time""" - - time: float - """Time in seconds""" - - -class MetricsFacebookPostMetricsDtoVideoRetentionGraphClickedToPlay(BaseModel): - rate: float - """Percentage of viewers at this time""" - - time: float - """Time in seconds""" - - -class MetricsFacebookPostMetricsDtoVideoViewTimeByAgeGender(BaseModel): - key: str - """Demographic key (e.g., age_gender, region, country)""" - - value: float - """Total view time in milliseconds""" - - -class MetricsFacebookPostMetricsDtoVideoViewTimeByCountry(BaseModel): - key: str - """Demographic key (e.g., age_gender, region, country)""" - - value: float - """Total view time in milliseconds""" - - -class MetricsFacebookPostMetricsDtoVideoViewTimeByRegion(BaseModel): - key: str - """Demographic key (e.g., age_gender, region, country)""" - - value: float - """Total view time in milliseconds""" - - class MetricsFacebookPostMetricsDto(BaseModel): - activity_by_action_type: Optional[List[MetricsFacebookPostMetricsDtoActivityByActionType]] = None + activity_by_action_type: Optional[List[FacebookActivityByActionType]] = None """Total activity breakdown by action type""" - activity_by_action_type_unique: Optional[List[MetricsFacebookPostMetricsDtoActivityByActionTypeUnique]] = None + activity_by_action_type_unique: Optional[List[FacebookActivityByActionType]] = None """Unique users activity breakdown by action type""" comments: Optional[float] = None @@ -447,12 +369,10 @@ class MetricsFacebookPostMetricsDto(BaseModel): video_length: Optional[float] = None """Length of the video in milliseconds""" - video_retention_graph_autoplayed: Optional[List[MetricsFacebookPostMetricsDtoVideoRetentionGraphAutoplayed]] = None + video_retention_graph_autoplayed: Optional[List[FacebookVideoRetentionGraph]] = None """Video retention graph for autoplayed views""" - video_retention_graph_clicked_to_play: Optional[ - List[MetricsFacebookPostMetricsDtoVideoRetentionGraphClickedToPlay] - ] = None + video_retention_graph_clicked_to_play: Optional[List[FacebookVideoRetentionGraph]] = None """Video retention graph for clicked-to-play views""" video_social_actions_unique: Optional[float] = None @@ -461,16 +381,16 @@ class MetricsFacebookPostMetricsDto(BaseModel): video_view_time: Optional[float] = None """Total time video was viewed in milliseconds""" - video_view_time_by_age_gender: Optional[List[MetricsFacebookPostMetricsDtoVideoViewTimeByAgeGender]] = None + video_view_time_by_age_gender: Optional[List[FacebookVideoViewTimeByDemographic]] = None """Video view time breakdown by age and gender""" - video_view_time_by_country: Optional[List[MetricsFacebookPostMetricsDtoVideoViewTimeByCountry]] = None + video_view_time_by_country: Optional[List[FacebookVideoViewTimeByDemographic]] = None """Video view time breakdown by country""" video_view_time_by_distribution_type: Optional[object] = None """Video view time breakdown by distribution type""" - video_view_time_by_region: Optional[List[MetricsFacebookPostMetricsDtoVideoViewTimeByRegion]] = None + video_view_time_by_region: Optional[List[FacebookVideoViewTimeByDemographic]] = None """Video view time breakdown by region""" video_view_time_organic: Optional[float] = None @@ -675,103 +595,11 @@ class MetricsBlueskyPostMetricsDto(BaseModel): """Number of reposts of the post""" -class MetricsPinterestPostMetricsDto_90d(BaseModel): - """Last 90 days of Pin metrics""" - - comment: Optional[float] = None - """Number of comments on the Pin""" - - impression: Optional[float] = None - """Number of times the Pin was shown (impressions)""" - - last_updated: Optional[str] = None - """The last time Pinterest updated these metrics""" - - outbound_click: Optional[float] = None - """Number of clicks from the Pin to an external destination (outbound clicks)""" - - pin_click: Optional[float] = None - """Number of clicks on the Pin to view it in closeup (Pin clicks)""" - - profile_visit: Optional[object] = None - """Number of visits to the author's profile driven from the Pin""" - - reaction: Optional[float] = None - """Total number of reactions on the Pin""" - - save: Optional[float] = None - """Number of saves of the Pin""" - - user_follow: Optional[object] = None - """Number of follows driven from the Pin""" - - video_10s_views: Optional[float] = None - """Number of video views of at least 10 seconds""" - - video_average_time: Optional[float] = None - """Average watch time for the video""" - - video_p95_views: Optional[float] = None - """Number of video views that reached 95% completion""" - - video_total_time: Optional[float] = None - """Total watch time for the video""" - - video_views: Optional[float] = None - """Number of video views""" - - -class MetricsPinterestPostMetricsDtoLifetimeMetrics(BaseModel): - """Lifetime Pin metrics""" - - comment: Optional[float] = None - """Number of comments on the Pin""" - - impression: Optional[float] = None - """Number of times the Pin was shown (impressions)""" - - last_updated: Optional[str] = None - """The last time Pinterest updated these metrics""" - - outbound_click: Optional[float] = None - """Number of clicks from the Pin to an external destination (outbound clicks)""" - - pin_click: Optional[float] = None - """Number of clicks on the Pin to view it in closeup (Pin clicks)""" - - profile_visit: Optional[object] = None - """Number of visits to the author's profile driven from the Pin""" - - reaction: Optional[float] = None - """Total number of reactions on the Pin""" - - save: Optional[float] = None - """Number of saves of the Pin""" - - user_follow: Optional[object] = None - """Number of follows driven from the Pin""" - - video_10s_views: Optional[float] = None - """Number of video views of at least 10 seconds""" - - video_average_time: Optional[float] = None - """Average watch time for the video""" - - video_p95_views: Optional[float] = None - """Number of video views that reached 95% completion""" - - video_total_time: Optional[float] = None - """Total watch time for the video""" - - video_views: Optional[float] = None - """Number of video views""" - - class MetricsPinterestPostMetricsDto(BaseModel): - api_90d: Optional[MetricsPinterestPostMetricsDto_90d] = FieldInfo(alias="90d", default=None) + api_90d: Optional[PinterestMetricsWindow] = FieldInfo(alias="90d", default=None) """Last 90 days of Pin metrics""" - lifetime_metrics: Optional[MetricsPinterestPostMetricsDtoLifetimeMetrics] = None + lifetime_metrics: Optional[PinterestMetricsWindow] = None """Lifetime Pin metrics""" @@ -789,13 +617,6 @@ class MetricsPinterestPostMetricsDto(BaseModel): ] -class PlatformData(BaseModel): - """Platform-specific data for the post""" - - title: str - """Title of the post""" - - class PlatformPost(BaseModel): caption: str """Caption or text content of the post""" @@ -827,7 +648,7 @@ class PlatformPost(BaseModel): metrics: Optional[Metrics] = None """Post metrics and analytics data""" - platform_data: Optional[PlatformData] = None + platform_data: Optional[YoutubePostPlatformData] = None """Platform-specific data for the post""" posted_at: Optional[datetime] = None diff --git a/src/post_for_me/types/social_account_create_params.py b/src/post_for_me/types/social_account_create_params.py index 15fc13b..c9e9bee 100644 --- a/src/post_for_me/types/social_account_create_params.py +++ b/src/post_for_me/types/social_account_create_params.py @@ -7,6 +7,7 @@ from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo +from .social_account_metadata_param import SocialAccountMetadataParam __all__ = ["SocialAccountCreateParams"] @@ -40,7 +41,7 @@ class SocialAccountCreateParams(TypedDict, total=False): external_id: Optional[str] """The external id of the social account""" - metadata: object + metadata: SocialAccountMetadataParam """The metadata of the social account""" refresh_token: Optional[str] diff --git a/src/post_for_me/types/social_account_metadata_param.py b/src/post_for_me/types/social_account_metadata_param.py new file mode 100644 index 0000000..649c9ca --- /dev/null +++ b/src/post_for_me/types/social_account_metadata_param.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypeAlias + +__all__ = ["SocialAccountMetadataParam"] + +SocialAccountMetadataParam: TypeAlias = object diff --git a/src/post_for_me/types/social_post.py b/src/post_for_me/types/social_post.py index 9cf83cc..d492426 100644 --- a/src/post_for_me/types/social_post.py +++ b/src/post_for_me/types/social_post.py @@ -5,232 +5,11 @@ from .._models import BaseModel from .social_account import SocialAccount +from .social_post_media import SocialPostMedia +from .account_configuration import AccountConfiguration from .platform_configurations_dto import PlatformConfigurationsDto -__all__ = [ - "SocialPost", - "AccountConfiguration", - "AccountConfigurationConfiguration", - "AccountConfigurationConfigurationMedia", - "AccountConfigurationConfigurationMediaTag", - "AccountConfigurationConfigurationPoll", - "Media", - "MediaTag", -] - - -class AccountConfigurationConfigurationMediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class AccountConfigurationConfigurationMedia(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[AccountConfigurationConfigurationMediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" - - -class AccountConfigurationConfigurationPoll(BaseModel): - """Poll options for the twitter""" - - duration_minutes: float - """Duration of the poll in minutes""" - - options: List[str] - """The choices of the poll, requiring 2-4 options""" - - reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] = None - """Who can reply to the tweet""" - - -class AccountConfigurationConfiguration(BaseModel): - """Configuration for the social account""" - - allow_comment: Optional[bool] = None - """Allow comments on TikTok""" - - allow_duet: Optional[bool] = None - """Allow duets on TikTok""" - - allow_stitch: Optional[bool] = None - """Allow stitch on TikTok""" - - auto_add_music: Optional[bool] = None - """Will automatically add music to photo posts on TikTok""" - - board_ids: Optional[List[str]] = None - """Pinterest board IDs""" - - caption: Optional[object] = None - """Overrides the `caption` from the post""" - - collaborators: Optional[List[List[object]]] = None - """ - List of page ids or users to invite as collaborators for a Video Reel (Instagram - and Facebook) - """ - - community_id: Optional[str] = None - """Id of the twitter community to post to""" - - disclose_branded_content: Optional[bool] = None - """Disclose branded content on TikTok""" - - disclose_your_brand: Optional[bool] = None - """Disclose your brand on TikTok""" - - is_ai_generated: Optional[bool] = None - """Flag content as AI generated on TikTok""" - - is_draft: Optional[bool] = None - """ - Will create a draft upload to TikTok, posting will need to be completed from - within the app - """ - - link: Optional[str] = None - """Pinterest post link""" - - location: Optional[str] = None - """ - Page id with a location that you want to tag the image or video with (Instagram - and Facebook) - """ - - made_for_kids: Optional[bool] = None - """If true will notify YouTube the video is intended for kids, defaults to false""" - - media: Optional[List[AccountConfigurationConfigurationMedia]] = None - """Overrides the `media` from the post""" - - placement: Optional[Literal["reels", "timeline", "stories"]] = None - """Post placement for Facebook/Instagram/Threads""" - - poll: Optional[AccountConfigurationConfigurationPoll] = None - """Poll options for the twitter""" - - privacy_status: Optional[Literal["public", "private", "unlisted"]] = None - """ - Sets the privacy status for TikTok (private, public), or YouTube (private, - public, unlisted) - """ - - quote_tweet_id: Optional[str] = None - """Id of the tweet you want to quote""" - - reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] = None - """Who can reply to the tweet""" - - set_caption_for_each_image: Optional[bool] = None - """ - If true, include the caption on each image in a Facebook carousel upload; if - false, only include it on the final carousel post - """ - - share_to_feed: Optional[bool] = None - """If false Instagram video posts will only be shown in the Reels tab""" - - title: Optional[str] = None - """Overrides the `title` from the post""" - - trial_reel_type: Optional[Literal["manual", "performance"]] = None - """Instagram trial reel type, when passed will be created as a trial reel. - - If manual the trial reel can be manually graduated in the native app. If - perfomance the trial reel will be automatically graduated if the trial reel - performs well. - """ - - -class AccountConfiguration(BaseModel): - configuration: AccountConfigurationConfiguration - """Configuration for the social account""" - - social_account_id: str - """ID of the social account, you want to apply the configuration to""" - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["SocialPost"] class SocialPost(BaseModel): @@ -249,7 +28,7 @@ class SocialPost(BaseModel): external_id: Optional[str] = None """Provided unique identifier of the post""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Array of media associated with the post""" platform_configurations: Optional[PlatformConfigurationsDto] = None diff --git a/src/post_for_me/types/social_post_create_params.py b/src/post_for_me/types/social_post_create_params.py index d456fbf..001ef0a 100644 --- a/src/post_for_me/types/social_post_create_params.py +++ b/src/post_for_me/types/social_post_create_params.py @@ -4,22 +4,15 @@ from typing import Union, Iterable, Optional from datetime import datetime -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo +from .social_post_media_param import SocialPostMediaParam +from .account_configuration_param import AccountConfigurationParam from .platform_configurations_dto_param import PlatformConfigurationsDtoParam -__all__ = [ - "SocialPostCreateParams", - "AccountConfiguration", - "AccountConfigurationConfiguration", - "AccountConfigurationConfigurationMedia", - "AccountConfigurationConfigurationMediaTag", - "AccountConfigurationConfigurationPoll", - "Media", - "MediaTag", -] +__all__ = ["SocialPostCreateParams"] class SocialPostCreateParams(TypedDict, total=False): @@ -29,7 +22,7 @@ class SocialPostCreateParams(TypedDict, total=False): social_accounts: Required[SequenceNotStr[str]] """Array of social account IDs for posting""" - account_configurations: Optional[Iterable[AccountConfiguration]] + account_configurations: Optional[Iterable[AccountConfigurationParam]] """Account-specific configurations for the post""" external_id: Optional[str] @@ -38,7 +31,7 @@ class SocialPostCreateParams(TypedDict, total=False): is_draft: Annotated[Optional[bool], PropertyInfo(alias="isDraft")] """If isDraft is set then the post will not be processed""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Array of media associated with the post. If multiple media items are provided and the placement is `stories`, individual @@ -53,217 +46,3 @@ class SocialPostCreateParams(TypedDict, total=False): Scheduled date and time for the post, setting to null or undefined will post instantly """ - - -class AccountConfigurationConfigurationMediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class AccountConfigurationConfigurationMedia(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[AccountConfigurationConfigurationMediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" - - -class AccountConfigurationConfigurationPoll(TypedDict, total=False): - """Poll options for the twitter""" - - duration_minutes: Required[float] - """Duration of the poll in minutes""" - - options: Required[SequenceNotStr[str]] - """The choices of the poll, requiring 2-4 options""" - - reply_settings: Literal["following", "mentionedUsers", "subscribers", "verified"] - """Who can reply to the tweet""" - - -class AccountConfigurationConfiguration(TypedDict, total=False): - """Configuration for the social account""" - - allow_comment: Optional[bool] - """Allow comments on TikTok""" - - allow_duet: Optional[bool] - """Allow duets on TikTok""" - - allow_stitch: Optional[bool] - """Allow stitch on TikTok""" - - auto_add_music: Optional[bool] - """Will automatically add music to photo posts on TikTok""" - - board_ids: Optional[SequenceNotStr[str]] - """Pinterest board IDs""" - - caption: Optional[object] - """Overrides the `caption` from the post""" - - collaborators: Optional[Iterable[Iterable[object]]] - """ - List of page ids or users to invite as collaborators for a Video Reel (Instagram - and Facebook) - """ - - community_id: str - """Id of the twitter community to post to""" - - disclose_branded_content: Optional[bool] - """Disclose branded content on TikTok""" - - disclose_your_brand: Optional[bool] - """Disclose your brand on TikTok""" - - is_ai_generated: Optional[bool] - """Flag content as AI generated on TikTok""" - - is_draft: Optional[bool] - """ - Will create a draft upload to TikTok, posting will need to be completed from - within the app - """ - - link: Optional[str] - """Pinterest post link""" - - location: Optional[str] - """ - Page id with a location that you want to tag the image or video with (Instagram - and Facebook) - """ - - made_for_kids: Optional[bool] - """If true will notify YouTube the video is intended for kids, defaults to false""" - - media: Optional[Iterable[AccountConfigurationConfigurationMedia]] - """Overrides the `media` from the post""" - - placement: Optional[Literal["reels", "timeline", "stories"]] - """Post placement for Facebook/Instagram/Threads""" - - poll: AccountConfigurationConfigurationPoll - """Poll options for the twitter""" - - privacy_status: Optional[Literal["public", "private", "unlisted"]] - """ - Sets the privacy status for TikTok (private, public), or YouTube (private, - public, unlisted) - """ - - quote_tweet_id: str - """Id of the tweet you want to quote""" - - reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] - """Who can reply to the tweet""" - - set_caption_for_each_image: Optional[bool] - """ - If true, include the caption on each image in a Facebook carousel upload; if - false, only include it on the final carousel post - """ - - share_to_feed: Optional[bool] - """If false Instagram video posts will only be shown in the Reels tab""" - - title: Optional[str] - """Overrides the `title` from the post""" - - trial_reel_type: Optional[Literal["manual", "performance"]] - """Instagram trial reel type, when passed will be created as a trial reel. - - If manual the trial reel can be manually graduated in the native app. If - perfomance the trial reel will be automatically graduated if the trial reel - performs well. - """ - - -class AccountConfiguration(TypedDict, total=False): - configuration: Required[AccountConfigurationConfiguration] - """Configuration for the social account""" - - social_account_id: Required[str] - """ID of the social account, you want to apply the configuration to""" - - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" diff --git a/src/post_for_me/types/social_post_media.py b/src/post_for_me/types/social_post_media.py new file mode 100644 index 0000000..6a1577f --- /dev/null +++ b/src/post_for_me/types/social_post_media.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["SocialPostMedia", "Tag"] + + +class Tag(BaseModel): + id: str + """Facebook User ID, Instagram Username or Instagram product id to tag""" + + platform: Literal["facebook", "instagram"] + """The platform for the tags""" + + type: Literal["user", "product"] + """ + The type of tag, user to tag accounts, product to tag products (only supported + for instagram) + """ + + x: Optional[float] = None + """ + Percentage distance from left edge of the image, Not required for videos or + stories + """ + + y: Optional[float] = None + """ + Percentage distance from top edge of the image, Not required for videos or + stories + """ + + +class SocialPostMedia(BaseModel): + url: str + """Public URL of the media""" + + skip_processing: Optional[bool] = None + """ + If true the media will not be processed at all and instead be posted as is, this + may increase chance of post failure if media does not meet platform's + requirements. Best used for larger files. + """ + + tags: Optional[List[Tag]] = None + """List of tags to attach to the media""" + + thumbnail_timestamp_ms: Optional[object] = None + """Timestamp in milliseconds of frame to use as thumbnail for the media""" + + thumbnail_url: Optional[object] = None + """Public URL of the thumbnail for the media""" diff --git a/src/post_for_me/types/social_post_media_param.py b/src/post_for_me/types/social_post_media_param.py new file mode 100644 index 0000000..4c2079a --- /dev/null +++ b/src/post_for_me/types/social_post_media_param.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["SocialPostMediaParam", "Tag"] + + +class Tag(TypedDict, total=False): + id: Required[str] + """Facebook User ID, Instagram Username or Instagram product id to tag""" + + platform: Required[Literal["facebook", "instagram"]] + """The platform for the tags""" + + type: Required[Literal["user", "product"]] + """ + The type of tag, user to tag accounts, product to tag products (only supported + for instagram) + """ + + x: float + """ + Percentage distance from left edge of the image, Not required for videos or + stories + """ + + y: float + """ + Percentage distance from top edge of the image, Not required for videos or + stories + """ + + +class SocialPostMediaParam(TypedDict, total=False): + url: Required[str] + """Public URL of the media""" + + skip_processing: Optional[bool] + """ + If true the media will not be processed at all and instead be posted as is, this + may increase chance of post failure if media does not meet platform's + requirements. Best used for larger files. + """ + + tags: Optional[Iterable[Tag]] + """List of tags to attach to the media""" + + thumbnail_timestamp_ms: Optional[object] + """Timestamp in milliseconds of frame to use as thumbnail for the media""" + + thumbnail_url: Optional[object] + """Public URL of the thumbnail for the media""" diff --git a/src/post_for_me/types/social_post_preview.py b/src/post_for_me/types/social_post_preview.py new file mode 100644 index 0000000..0e6a932 --- /dev/null +++ b/src/post_for_me/types/social_post_preview.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .social_post_media import SocialPostMedia + +__all__ = ["SocialPostPreview"] + + +class SocialPostPreview(BaseModel): + caption: str + """Caption text for the post""" + + platform: str + """Platform of the post""" + + social_account_id: str + """Id of the social account""" + + configuration: Optional[object] = None + """Additional configuration for this platform""" + + media: Optional[List[SocialPostMedia]] = None + """Array of media URLs associated with the post""" + + social_account_profile_picture_url: Optional[object] = None + """Url of the social account profile picture""" + + social_account_username: Optional[object] = None + """Username of the social account""" diff --git a/src/post_for_me/types/social_post_preview_create_params.py b/src/post_for_me/types/social_post_preview_create_params.py new file mode 100644 index 0000000..c9d8cfd --- /dev/null +++ b/src/post_for_me/types/social_post_preview_create_params.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable, Optional +from typing_extensions import Required, TypedDict + +from .social_post_media_param import SocialPostMediaParam +from .account_configuration_param import AccountConfigurationParam +from .platform_configurations_dto_param import PlatformConfigurationsDtoParam + +__all__ = ["SocialPostPreviewCreateParams", "PreviewSocialAccount"] + + +class SocialPostPreviewCreateParams(TypedDict, total=False): + caption: Required[str] + """Caption text for the post""" + + preview_social_accounts: Required[Iterable[PreviewSocialAccount]] + """Array of social accounts. + + Can preview non connected accounts, just specify a random ID + """ + + account_configurations: Optional[Iterable[AccountConfigurationParam]] + """Account-specific configurations for the post""" + + media: Optional[Iterable[SocialPostMediaParam]] + """Array of media URLs associated with the post""" + + platform_configurations: Optional[PlatformConfigurationsDtoParam] + """Platform-specific configurations for the post""" + + +class PreviewSocialAccount(TypedDict, total=False): + id: Required[str] + """ID of the social account, ex: spc_12312""" + + platform: Required[str] + """Platform of the social account""" + + username: str + """username of the social account""" diff --git a/src/post_for_me/types/social_post_preview_create_response.py b/src/post_for_me/types/social_post_preview_create_response.py new file mode 100644 index 0000000..624108c --- /dev/null +++ b/src/post_for_me/types/social_post_preview_create_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .social_post_preview import SocialPostPreview + +__all__ = ["SocialPostPreviewCreateResponse"] + +SocialPostPreviewCreateResponse: TypeAlias = List[SocialPostPreview] diff --git a/src/post_for_me/types/social_post_result.py b/src/post_for_me/types/social_post_result.py index c29fbda..ef0bd03 100644 --- a/src/post_for_me/types/social_post_result.py +++ b/src/post_for_me/types/social_post_result.py @@ -1,58 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["SocialPostResult", "Media", "MediaTag", "PlatformData"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["SocialPostResult", "PlatformData"] class PlatformData(BaseModel): @@ -75,7 +28,7 @@ class SocialPostResult(BaseModel): error: object """Error message if the post failed""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Array of media URLs associated with the post""" platform_data: PlatformData diff --git a/src/post_for_me/types/social_post_update_params.py b/src/post_for_me/types/social_post_update_params.py index fa55930..0d89a1b 100644 --- a/src/post_for_me/types/social_post_update_params.py +++ b/src/post_for_me/types/social_post_update_params.py @@ -4,22 +4,15 @@ from typing import Union, Iterable, Optional from datetime import datetime -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Required, Annotated, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo +from .social_post_media_param import SocialPostMediaParam +from .account_configuration_param import AccountConfigurationParam from .platform_configurations_dto_param import PlatformConfigurationsDtoParam -__all__ = [ - "SocialPostUpdateParams", - "AccountConfiguration", - "AccountConfigurationConfiguration", - "AccountConfigurationConfigurationMedia", - "AccountConfigurationConfigurationMediaTag", - "AccountConfigurationConfigurationPoll", - "Media", - "MediaTag", -] +__all__ = ["SocialPostUpdateParams"] class SocialPostUpdateParams(TypedDict, total=False): @@ -29,7 +22,7 @@ class SocialPostUpdateParams(TypedDict, total=False): social_accounts: Required[SequenceNotStr[str]] """Array of social account IDs for posting""" - account_configurations: Optional[Iterable[AccountConfiguration]] + account_configurations: Optional[Iterable[AccountConfigurationParam]] """Account-specific configurations for the post""" external_id: Optional[str] @@ -38,7 +31,7 @@ class SocialPostUpdateParams(TypedDict, total=False): is_draft: Annotated[Optional[bool], PropertyInfo(alias="isDraft")] """If isDraft is set then the post will not be processed""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Array of media associated with the post. If multiple media items are provided and the placement is `stories`, individual @@ -53,217 +46,3 @@ class SocialPostUpdateParams(TypedDict, total=False): Scheduled date and time for the post, setting to null or undefined will post instantly """ - - -class AccountConfigurationConfigurationMediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class AccountConfigurationConfigurationMedia(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[AccountConfigurationConfigurationMediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" - - -class AccountConfigurationConfigurationPoll(TypedDict, total=False): - """Poll options for the twitter""" - - duration_minutes: Required[float] - """Duration of the poll in minutes""" - - options: Required[SequenceNotStr[str]] - """The choices of the poll, requiring 2-4 options""" - - reply_settings: Literal["following", "mentionedUsers", "subscribers", "verified"] - """Who can reply to the tweet""" - - -class AccountConfigurationConfiguration(TypedDict, total=False): - """Configuration for the social account""" - - allow_comment: Optional[bool] - """Allow comments on TikTok""" - - allow_duet: Optional[bool] - """Allow duets on TikTok""" - - allow_stitch: Optional[bool] - """Allow stitch on TikTok""" - - auto_add_music: Optional[bool] - """Will automatically add music to photo posts on TikTok""" - - board_ids: Optional[SequenceNotStr[str]] - """Pinterest board IDs""" - - caption: Optional[object] - """Overrides the `caption` from the post""" - - collaborators: Optional[Iterable[Iterable[object]]] - """ - List of page ids or users to invite as collaborators for a Video Reel (Instagram - and Facebook) - """ - - community_id: str - """Id of the twitter community to post to""" - - disclose_branded_content: Optional[bool] - """Disclose branded content on TikTok""" - - disclose_your_brand: Optional[bool] - """Disclose your brand on TikTok""" - - is_ai_generated: Optional[bool] - """Flag content as AI generated on TikTok""" - - is_draft: Optional[bool] - """ - Will create a draft upload to TikTok, posting will need to be completed from - within the app - """ - - link: Optional[str] - """Pinterest post link""" - - location: Optional[str] - """ - Page id with a location that you want to tag the image or video with (Instagram - and Facebook) - """ - - made_for_kids: Optional[bool] - """If true will notify YouTube the video is intended for kids, defaults to false""" - - media: Optional[Iterable[AccountConfigurationConfigurationMedia]] - """Overrides the `media` from the post""" - - placement: Optional[Literal["reels", "timeline", "stories"]] - """Post placement for Facebook/Instagram/Threads""" - - poll: AccountConfigurationConfigurationPoll - """Poll options for the twitter""" - - privacy_status: Optional[Literal["public", "private", "unlisted"]] - """ - Sets the privacy status for TikTok (private, public), or YouTube (private, - public, unlisted) - """ - - quote_tweet_id: str - """Id of the tweet you want to quote""" - - reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] - """Who can reply to the tweet""" - - set_caption_for_each_image: Optional[bool] - """ - If true, include the caption on each image in a Facebook carousel upload; if - false, only include it on the final carousel post - """ - - share_to_feed: Optional[bool] - """If false Instagram video posts will only be shown in the Reels tab""" - - title: Optional[str] - """Overrides the `title` from the post""" - - trial_reel_type: Optional[Literal["manual", "performance"]] - """Instagram trial reel type, when passed will be created as a trial reel. - - If manual the trial reel can be manually graduated in the native app. If - perfomance the trial reel will be automatically graduated if the trial reel - performs well. - """ - - -class AccountConfiguration(TypedDict, total=False): - configuration: Required[AccountConfigurationConfiguration] - """Configuration for the social account""" - - social_account_id: Required[str] - """ID of the social account, you want to apply the configuration to""" - - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" diff --git a/src/post_for_me/types/threads_configuration_dto.py b/src/post_for_me/types/threads_configuration_dto.py index 7bd6e43..aa10440 100644 --- a/src/post_for_me/types/threads_configuration_dto.py +++ b/src/post_for_me/types/threads_configuration_dto.py @@ -4,62 +4,16 @@ from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["ThreadsConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["ThreadsConfigurationDto"] class ThreadsConfigurationDto(BaseModel): caption: Optional[object] = None """Overrides the `caption` from the post""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" placement: Optional[Literal["reels", "timeline"]] = None diff --git a/src/post_for_me/types/threads_configuration_dto_param.py b/src/post_for_me/types/threads_configuration_dto_param.py index 59e29f2..d872b4b 100644 --- a/src/post_for_me/types/threads_configuration_dto_param.py +++ b/src/post_for_me/types/threads_configuration_dto_param.py @@ -3,63 +3,18 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict -__all__ = ["ThreadsConfigurationDtoParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["ThreadsConfigurationDtoParam"] class ThreadsConfigurationDtoParam(TypedDict, total=False): caption: Optional[object] """Overrides the `caption` from the post""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" placement: Optional[Literal["reels", "timeline"]] diff --git a/src/post_for_me/types/tiktok_business_video_metric_percentage.py b/src/post_for_me/types/tiktok_business_video_metric_percentage.py new file mode 100644 index 0000000..4d2d561 --- /dev/null +++ b/src/post_for_me/types/tiktok_business_video_metric_percentage.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["TiktokBusinessVideoMetricPercentage"] + + +class TiktokBusinessVideoMetricPercentage(BaseModel): + percentage: float + """Percentage value for the metric""" + + second: str + """Time in seconds for the metric""" diff --git a/src/post_for_me/types/tiktok_configuration.py b/src/post_for_me/types/tiktok_configuration.py index c367434..db3af61 100644 --- a/src/post_for_me/types/tiktok_configuration.py +++ b/src/post_for_me/types/tiktok_configuration.py @@ -1,58 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["TiktokConfiguration", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["TiktokConfiguration"] class TiktokConfiguration(BaseModel): @@ -86,7 +39,7 @@ class TiktokConfiguration(BaseModel): within the app """ - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" privacy_status: Optional[str] = None diff --git a/src/post_for_me/types/tiktok_configuration_param.py b/src/post_for_me/types/tiktok_configuration_param.py index 84afe86..254bc9e 100644 --- a/src/post_for_me/types/tiktok_configuration_param.py +++ b/src/post_for_me/types/tiktok_configuration_param.py @@ -3,56 +3,11 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import TypedDict -__all__ = ["TiktokConfigurationParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["TiktokConfigurationParam"] class TiktokConfigurationParam(TypedDict, total=False): @@ -86,7 +41,7 @@ class TiktokConfigurationParam(TypedDict, total=False): within the app """ - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" privacy_status: Optional[str] diff --git a/src/post_for_me/types/twitter_configuration_dto.py b/src/post_for_me/types/twitter_configuration_dto.py index 1237d86..833bdfa 100644 --- a/src/post_for_me/types/twitter_configuration_dto.py +++ b/src/post_for_me/types/twitter_configuration_dto.py @@ -4,68 +4,10 @@ from typing_extensions import Literal from .._models import BaseModel +from .twitter_poll import TwitterPoll +from .social_post_media import SocialPostMedia -__all__ = ["TwitterConfigurationDto", "Media", "MediaTag", "Poll"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" - - -class Poll(BaseModel): - """Poll options for the tweet""" - - duration_minutes: float - """Duration of the poll in minutes""" - - options: List[str] - """The choices of the poll, requiring 2-4 options""" - - reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] = None - """Who can reply to the tweet""" +__all__ = ["TwitterConfigurationDto"] class TwitterConfigurationDto(BaseModel): @@ -75,10 +17,10 @@ class TwitterConfigurationDto(BaseModel): community_id: Optional[str] = None """Id of the community to post to""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" - poll: Optional[Poll] = None + poll: Optional[TwitterPoll] = None """Poll options for the tweet""" quote_tweet_id: Optional[str] = None diff --git a/src/post_for_me/types/twitter_configuration_dto_param.py b/src/post_for_me/types/twitter_configuration_dto_param.py index 32baeb6..bde0686 100644 --- a/src/post_for_me/types/twitter_configuration_dto_param.py +++ b/src/post_for_me/types/twitter_configuration_dto_param.py @@ -3,71 +3,12 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict -from .._types import SequenceNotStr +from .twitter_poll_param import TwitterPollParam +from .social_post_media_param import SocialPostMediaParam -__all__ = ["TwitterConfigurationDtoParam", "Media", "MediaTag", "Poll"] - - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" - - -class Poll(TypedDict, total=False): - """Poll options for the tweet""" - - duration_minutes: Required[float] - """Duration of the poll in minutes""" - - options: Required[SequenceNotStr[str]] - """The choices of the poll, requiring 2-4 options""" - - reply_settings: Literal["following", "mentionedUsers", "subscribers", "verified"] - """Who can reply to the tweet""" +__all__ = ["TwitterConfigurationDtoParam"] class TwitterConfigurationDtoParam(TypedDict, total=False): @@ -77,10 +18,10 @@ class TwitterConfigurationDtoParam(TypedDict, total=False): community_id: str """Id of the community to post to""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" - poll: Poll + poll: TwitterPollParam """Poll options for the tweet""" quote_tweet_id: str diff --git a/src/post_for_me/types/twitter_poll.py b/src/post_for_me/types/twitter_poll.py new file mode 100644 index 0000000..01c4c9b --- /dev/null +++ b/src/post_for_me/types/twitter_poll.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["TwitterPoll"] + + +class TwitterPoll(BaseModel): + duration_minutes: float + """Duration of the poll in minutes""" + + options: List[str] + """The choices of the poll, requiring 2-4 options""" + + reply_settings: Optional[Literal["following", "mentionedUsers", "subscribers", "verified"]] = None + """Who can reply to the tweet""" diff --git a/src/post_for_me/types/twitter_poll_param.py b/src/post_for_me/types/twitter_poll_param.py new file mode 100644 index 0000000..2f2013a --- /dev/null +++ b/src/post_for_me/types/twitter_poll_param.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["TwitterPollParam"] + + +class TwitterPollParam(TypedDict, total=False): + duration_minutes: Required[float] + """Duration of the poll in minutes""" + + options: Required[SequenceNotStr[str]] + """The choices of the poll, requiring 2-4 options""" + + reply_settings: Literal["following", "mentionedUsers", "subscribers", "verified"] + """Who can reply to the tweet""" diff --git a/src/post_for_me/types/webhook.py b/src/post_for_me/types/webhook.py new file mode 100644 index 0000000..d55b0ae --- /dev/null +++ b/src/post_for_me/types/webhook.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["Webhook"] + + +class Webhook(BaseModel): + id: str + """The unique identifier of the webhook""" + + event_types: List[str] + """Events that will be sent to the webhook""" + + secret: str + """Secret key used to verify webhook post""" + + url: str + """The public webhook url""" diff --git a/src/post_for_me/types/webhook_create_params.py b/src/post_for_me/types/webhook_create_params.py new file mode 100644 index 0000000..9b25e0b --- /dev/null +++ b/src/post_for_me/types/webhook_create_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["WebhookCreateParams"] + + +class WebhookCreateParams(TypedDict, total=False): + event_types: Required[ + List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ] + ] + """List of events the webhook will recieve""" + + url: Required[str] + """Public url to recieve event data""" diff --git a/src/post_for_me/types/webhook_list_params.py b/src/post_for_me/types/webhook_list_params.py new file mode 100644 index 0000000..bce2d52 --- /dev/null +++ b/src/post_for_me/types/webhook_list_params.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .._types import SequenceNotStr + +__all__ = ["WebhookListParams"] + + +class WebhookListParams(TypedDict, total=False): + id: SequenceNotStr[str] + """Filter by id(s). + + Multiple values imply OR logic (e.g., ?id=wbh_xxxxxx&id=wbh_yyyyyy). + """ + + event_type: SequenceNotStr[str] + """Filter by event type(s). + + Multiple values imply OR logic (e.g., + ?event_type=social.post.created&event_type=social.post.updated). + """ + + limit: float + """Number of items to return""" + + offset: float + """Number of items to skip""" + + url: SequenceNotStr[str] + """Filter by url(s). + + Multiple values imply OR logic (e.g., + ?url=https://example.com&url=https://postforme.dev). + """ diff --git a/src/post_for_me/types/webhook_list_response.py b/src/post_for_me/types/webhook_list_response.py new file mode 100644 index 0000000..20b5a26 --- /dev/null +++ b/src/post_for_me/types/webhook_list_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .webhook import Webhook +from .._models import BaseModel + +__all__ = ["WebhookListResponse", "Meta"] + + +class Meta(BaseModel): + limit: float + """Maximum number of items returned.""" + + next: Optional[str] = None + """URL to the next page of results, or null if none.""" + + offset: float + """Number of items skipped.""" + + total: float + """Total number of items available.""" + + +class WebhookListResponse(BaseModel): + data: List[Webhook] + + meta: Meta diff --git a/src/post_for_me/types/webhook_update_params.py b/src/post_for_me/types/webhook_update_params.py new file mode 100644 index 0000000..972aeb7 --- /dev/null +++ b/src/post_for_me/types/webhook_update_params.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, TypedDict + +__all__ = ["WebhookUpdateParams"] + + +class WebhookUpdateParams(TypedDict, total=False): + event_types: List[ + Literal[ + "social.post.created", + "social.post.updated", + "social.post.deleted", + "social.post.result.created", + "social.account.created", + "social.account.updated", + ] + ] + """List of events the webhook will recieve""" + + url: str + """Public url to recieve event data""" diff --git a/src/post_for_me/types/youtube_configuration_dto.py b/src/post_for_me/types/youtube_configuration_dto.py index 87f3cd8..71bcf6f 100644 --- a/src/post_for_me/types/youtube_configuration_dto.py +++ b/src/post_for_me/types/youtube_configuration_dto.py @@ -4,55 +4,9 @@ from typing_extensions import Literal from .._models import BaseModel +from .social_post_media import SocialPostMedia -__all__ = ["YoutubeConfigurationDto", "Media", "MediaTag"] - - -class MediaTag(BaseModel): - id: str - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Literal["facebook", "instagram"] - """The platform for the tags""" - - type: Literal["user", "product"] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: Optional[float] = None - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: Optional[float] = None - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(BaseModel): - url: str - """Public URL of the media""" - - skip_processing: Optional[bool] = None - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[List[MediaTag]] = None - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] = None - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] = None - """Public URL of the thumbnail for the media""" +__all__ = ["YoutubeConfigurationDto"] class YoutubeConfigurationDto(BaseModel): @@ -62,7 +16,7 @@ class YoutubeConfigurationDto(BaseModel): made_for_kids: Optional[bool] = None """If true will notify YouTube the video is intended for kids, defaults to false""" - media: Optional[List[Media]] = None + media: Optional[List[SocialPostMedia]] = None """Overrides the `media` from the post""" privacy_status: Optional[Literal["public", "private", "unlisted"]] = None diff --git a/src/post_for_me/types/youtube_configuration_dto_param.py b/src/post_for_me/types/youtube_configuration_dto_param.py index c44e625..d0ab01c 100644 --- a/src/post_for_me/types/youtube_configuration_dto_param.py +++ b/src/post_for_me/types/youtube_configuration_dto_param.py @@ -3,56 +3,11 @@ from __future__ import annotations from typing import Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict -__all__ = ["YoutubeConfigurationDtoParam", "Media", "MediaTag"] +from .social_post_media_param import SocialPostMediaParam - -class MediaTag(TypedDict, total=False): - id: Required[str] - """Facebook User ID, Instagram Username or Instagram product id to tag""" - - platform: Required[Literal["facebook", "instagram"]] - """The platform for the tags""" - - type: Required[Literal["user", "product"]] - """ - The type of tag, user to tag accounts, product to tag products (only supported - for instagram) - """ - - x: float - """ - Percentage distance from left edge of the image, Not required for videos or - stories - """ - - y: float - """ - Percentage distance from top edge of the image, Not required for videos or - stories - """ - - -class Media(TypedDict, total=False): - url: Required[str] - """Public URL of the media""" - - skip_processing: Optional[bool] - """ - If true the media will not be processed at all and instead be posted as is, this - may increase chance of post failure if media does not meet platform's - requirements. Best used for larger files. - """ - - tags: Optional[Iterable[MediaTag]] - """List of tags to attach to the media""" - - thumbnail_timestamp_ms: Optional[object] - """Timestamp in milliseconds of frame to use as thumbnail for the media""" - - thumbnail_url: Optional[object] - """Public URL of the thumbnail for the media""" +__all__ = ["YoutubeConfigurationDtoParam"] class YoutubeConfigurationDtoParam(TypedDict, total=False): @@ -62,7 +17,7 @@ class YoutubeConfigurationDtoParam(TypedDict, total=False): made_for_kids: Optional[bool] """If true will notify YouTube the video is intended for kids, defaults to false""" - media: Optional[Iterable[Media]] + media: Optional[Iterable[SocialPostMediaParam]] """Overrides the `media` from the post""" privacy_status: Optional[Literal["public", "private", "unlisted"]] diff --git a/src/post_for_me/types/youtube_post_platform_data.py b/src/post_for_me/types/youtube_post_platform_data.py new file mode 100644 index 0000000..c447143 --- /dev/null +++ b/src/post_for_me/types/youtube_post_platform_data.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["YoutubePostPlatformData"] + + +class YoutubePostPlatformData(BaseModel): + title: str + """Title of the post""" diff --git a/tests/api_resources/test_social_post_previews.py b/tests/api_resources/test_social_post_previews.py new file mode 100644 index 0000000..b2fc3a1 --- /dev/null +++ b/tests/api_resources/test_social_post_previews.py @@ -0,0 +1,790 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from post_for_me import PostForMe, AsyncPostForMe +from tests.utils import assert_matches_type +from post_for_me.types import ( + SocialPostPreviewCreateResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSocialPostPreviews: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: PostForMe) -> None: + social_post_preview = client.social_post_previews.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: PostForMe) -> None: + social_post_preview = client.social_post_previews.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + "username": "username", + } + ], + account_configurations=[ + { + "configuration": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "board_ids": ["string"], + "caption": {}, + "collaborators": [[{}]], + "community_id": "community_id", + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "link": "link", + "location": "location", + "made_for_kids": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "poll": { + "duration_minutes": 0, + "options": ["string"], + "reply_settings": "following", + }, + "privacy_status": "public", + "quote_tweet_id": "quote_tweet_id", + "reply_settings": "following", + "set_caption_for_each_image": True, + "share_to_feed": True, + "title": "title", + "trial_reel_type": "manual", + }, + "social_account_id": "social_account_id", + } + ], + media=[ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + platform_configurations={ + "bluesky": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + }, + "facebook": { + "caption": {}, + "collaborators": [[{}]], + "location": "location", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "set_caption_for_each_image": True, + }, + "instagram": { + "caption": {}, + "collaborators": ["string"], + "location": "location", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "share_to_feed": True, + "trial_reel_type": "manual", + }, + "linkedin": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + }, + "pinterest": { + "board_ids": ["string"], + "caption": {}, + "link": "link", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "title": "title", + }, + "threads": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + }, + "tiktok": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "caption": {}, + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "privacy_status", + "title": "title", + }, + "tiktok_business": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "caption": {}, + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "privacy_status", + "title": "title", + }, + "x": { + "caption": {}, + "community_id": "community_id", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "poll": { + "duration_minutes": 0, + "options": ["string"], + "reply_settings": "following", + }, + "quote_tweet_id": "quote_tweet_id", + "reply_settings": "following", + }, + "youtube": { + "caption": {}, + "made_for_kids": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "public", + "title": "title", + }, + }, + ) + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: PostForMe) -> None: + response = client.social_post_previews.with_raw_response.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + social_post_preview = response.parse() + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: PostForMe) -> None: + with client.social_post_previews.with_streaming_response.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + social_post_preview = response.parse() + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncSocialPostPreviews: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncPostForMe) -> None: + social_post_preview = await async_client.social_post_previews.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncPostForMe) -> None: + social_post_preview = await async_client.social_post_previews.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + "username": "username", + } + ], + account_configurations=[ + { + "configuration": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "board_ids": ["string"], + "caption": {}, + "collaborators": [[{}]], + "community_id": "community_id", + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "link": "link", + "location": "location", + "made_for_kids": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "poll": { + "duration_minutes": 0, + "options": ["string"], + "reply_settings": "following", + }, + "privacy_status": "public", + "quote_tweet_id": "quote_tweet_id", + "reply_settings": "following", + "set_caption_for_each_image": True, + "share_to_feed": True, + "title": "title", + "trial_reel_type": "manual", + }, + "social_account_id": "social_account_id", + } + ], + media=[ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + platform_configurations={ + "bluesky": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + }, + "facebook": { + "caption": {}, + "collaborators": [[{}]], + "location": "location", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "set_caption_for_each_image": True, + }, + "instagram": { + "caption": {}, + "collaborators": ["string"], + "location": "location", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + "share_to_feed": True, + "trial_reel_type": "manual", + }, + "linkedin": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + }, + "pinterest": { + "board_ids": ["string"], + "caption": {}, + "link": "link", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "title": "title", + }, + "threads": { + "caption": {}, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "placement": "reels", + }, + "tiktok": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "caption": {}, + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "privacy_status", + "title": "title", + }, + "tiktok_business": { + "allow_comment": True, + "allow_duet": True, + "allow_stitch": True, + "auto_add_music": True, + "caption": {}, + "disclose_branded_content": True, + "disclose_your_brand": True, + "is_ai_generated": True, + "is_draft": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "privacy_status", + "title": "title", + }, + "x": { + "caption": {}, + "community_id": "community_id", + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "poll": { + "duration_minutes": 0, + "options": ["string"], + "reply_settings": "following", + }, + "quote_tweet_id": "quote_tweet_id", + "reply_settings": "following", + }, + "youtube": { + "caption": {}, + "made_for_kids": True, + "media": [ + { + "url": "url", + "skip_processing": True, + "tags": [ + { + "id": "id", + "platform": "facebook", + "type": "user", + "x": 0, + "y": 0, + } + ], + "thumbnail_timestamp_ms": {}, + "thumbnail_url": {}, + } + ], + "privacy_status": "public", + "title": "title", + }, + }, + ) + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncPostForMe) -> None: + response = await async_client.social_post_previews.with_raw_response.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + social_post_preview = await response.parse() + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncPostForMe) -> None: + async with async_client.social_post_previews.with_streaming_response.create( + caption="caption", + preview_social_accounts=[ + { + "id": "id", + "platform": "platform", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + social_post_preview = await response.parse() + assert_matches_type(SocialPostPreviewCreateResponse, social_post_preview, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_social_posts.py b/tests/api_resources/test_social_posts.py index b66c6b0..17ee9df 100644 --- a/tests/api_resources/test_social_posts.py +++ b/tests/api_resources/test_social_posts.py @@ -11,8 +11,8 @@ from tests.utils import assert_matches_type from post_for_me.types import ( SocialPost, + DeleteEntityResponse, SocialPostListResponse, - SocialPostDeleteResponse, ) from post_for_me._utils import parse_datetime @@ -219,6 +219,7 @@ def test_method_create_with_all_params(self, client: PostForMe) -> None: "thumbnail_url": {}, } ], + "title": "title", }, "threads": { "caption": {}, @@ -626,6 +627,7 @@ def test_method_update_with_all_params(self, client: PostForMe) -> None: "thumbnail_url": {}, } ], + "title": "title", }, "threads": { "caption": {}, @@ -851,7 +853,7 @@ def test_method_delete(self, client: PostForMe) -> None: social_post = client.social_posts.delete( "id", ) - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -863,7 +865,7 @@ def test_raw_response_delete(self, client: PostForMe) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" social_post = response.parse() - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -875,7 +877,7 @@ def test_streaming_response_delete(self, client: PostForMe) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" social_post = response.parse() - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) assert cast(Any, response.is_closed) is True @@ -1090,6 +1092,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPostForMe) "thumbnail_url": {}, } ], + "title": "title", }, "threads": { "caption": {}, @@ -1497,6 +1500,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncPostForMe) "thumbnail_url": {}, } ], + "title": "title", }, "threads": { "caption": {}, @@ -1722,7 +1726,7 @@ async def test_method_delete(self, async_client: AsyncPostForMe) -> None: social_post = await async_client.social_posts.delete( "id", ) - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -1734,7 +1738,7 @@ async def test_raw_response_delete(self, async_client: AsyncPostForMe) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" social_post = await response.parse() - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -1746,7 +1750,7 @@ async def test_streaming_response_delete(self, async_client: AsyncPostForMe) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" social_post = await response.parse() - assert_matches_type(SocialPostDeleteResponse, social_post, path=["response"]) + assert_matches_type(DeleteEntityResponse, social_post, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py new file mode 100644 index 0000000..318aea1 --- /dev/null +++ b/tests/api_resources/test_webhooks.py @@ -0,0 +1,454 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from post_for_me import PostForMe, AsyncPostForMe +from tests.utils import assert_matches_type +from post_for_me.types import ( + Webhook, + WebhookListResponse, + DeleteEntityResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestWebhooks: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: PostForMe) -> None: + webhook = client.webhooks.create( + event_types=["social.post.created"], + url="url", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: PostForMe) -> None: + response = client.webhooks.with_raw_response.create( + event_types=["social.post.created"], + url="url", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: PostForMe) -> None: + with client.webhooks.with_streaming_response.create( + event_types=["social.post.created"], + url="url", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: PostForMe) -> None: + webhook = client.webhooks.retrieve( + "id", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: PostForMe) -> None: + response = client.webhooks.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: PostForMe) -> None: + with client.webhooks.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: PostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.webhooks.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: PostForMe) -> None: + webhook = client.webhooks.update( + id="id", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: PostForMe) -> None: + webhook = client.webhooks.update( + id="id", + event_types=["social.post.created"], + url="url", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: PostForMe) -> None: + response = client.webhooks.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: PostForMe) -> None: + with client.webhooks.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: PostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.webhooks.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: PostForMe) -> None: + webhook = client.webhooks.list() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: PostForMe) -> None: + webhook = client.webhooks.list( + id=["string"], + event_type=["string"], + limit=0, + offset=0, + url=["string"], + ) + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: PostForMe) -> None: + response = client.webhooks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: PostForMe) -> None: + with client.webhooks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: PostForMe) -> None: + webhook = client.webhooks.delete( + "id", + ) + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: PostForMe) -> None: + response = client.webhooks.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: PostForMe) -> None: + with client.webhooks.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: PostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.webhooks.with_raw_response.delete( + "", + ) + + +class TestAsyncWebhooks: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.create( + event_types=["social.post.created"], + url="url", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncPostForMe) -> None: + response = await async_client.webhooks.with_raw_response.create( + event_types=["social.post.created"], + url="url", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncPostForMe) -> None: + async with async_client.webhooks.with_streaming_response.create( + event_types=["social.post.created"], + url="url", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.retrieve( + "id", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncPostForMe) -> None: + response = await async_client.webhooks.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncPostForMe) -> None: + async with async_client.webhooks.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncPostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.webhooks.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.update( + id="id", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.update( + id="id", + event_types=["social.post.created"], + url="url", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncPostForMe) -> None: + response = await async_client.webhooks.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncPostForMe) -> None: + async with async_client.webhooks.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncPostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.webhooks.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.list() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.list( + id=["string"], + event_type=["string"], + limit=0, + offset=0, + url=["string"], + ) + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncPostForMe) -> None: + response = await async_client.webhooks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncPostForMe) -> None: + async with async_client.webhooks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncPostForMe) -> None: + webhook = await async_client.webhooks.delete( + "id", + ) + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncPostForMe) -> None: + response = await async_client.webhooks.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncPostForMe) -> None: + async with async_client.webhooks.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(DeleteEntityResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncPostForMe) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.webhooks.with_raw_response.delete( + "", + ) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index d160965..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from post_for_me._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index e7aa16e..a17f687 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from post_for_me._types import FileTypes +from post_for_me._types import FileTypes, ArrayFormat from post_for_me._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index 9f789a5..c0c8374 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from post_for_me._files import to_httpx_files, async_to_httpx_files +from post_for_me._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from post_for_me._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert [entry for _, entry in extracted] == [file1, file2] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } diff --git a/tests/test_models.py b/tests/test_models.py index ad75540..da9c024 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from post_for_me._utils import PropertyInfo from post_for_me._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from post_for_me._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from post_for_me._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]