From dea20788c0471b1ee7db3e4b63aa7be2db47ae6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 18:00:20 +0000 Subject: [PATCH 01/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.28 --- openapi.yaml | 43 +- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/mcp/generated_tools.py | 2751 ++++++++++++++++-------- src/late/models/_generated/models.py | 2 +- src/late/resources/_generated/posts.py | 8 + uv.lock | 2 +- 7 files changed, 1950 insertions(+), 860 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index e5f1d4c..8f66556 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5366,14 +5366,21 @@ paths: tags: [Posts] summary: Update post metadata description: | - Updates metadata of an already-published post on the specified platform without re-uploading the media. - Currently only supported for YouTube videos (title, description, tags, category, privacy status). - The post must have "published" status on the target platform. At least one updatable field is required. + Updates metadata of a published video on the specified platform without re-uploading. + Currently only supported for YouTube. At least one updatable field is required. + + **Two modes:** + + 1. **Post-based** (video published through Zernio): pass the Zernio postId in the URL and `platform` in the body. + 2. **Direct video ID** (video uploaded outside Zernio, e.g. directly to YouTube): use `_` as the postId, + and pass `videoId` + `accountId` + `platform` in the body. The accountId is the Zernio social account ID + for the connected YouTube channel. parameters: - name: postId in: path required: true schema: { type: string } + description: Zernio post ID, or "_" when using direct video ID mode requestBody: required: true content: @@ -5387,6 +5394,12 @@ paths: description: The platform to update metadata on enum: - youtube + videoId: + type: string + description: YouTube video ID (required for direct mode, ignored for post-based mode) + accountId: + type: string + description: Zernio social account ID (required for direct mode, ignored for post-based mode) title: type: string maxLength: 100 @@ -5407,11 +5420,22 @@ paths: type: string enum: [public, private, unlisted] description: Video privacy setting - example: - platform: "youtube" - title: "Updated Video Title" - description: "New SEO-optimized description" - tags: ["seo", "marketing", "tutorial"] + examples: + post-based: + summary: Update a video published through Zernio + value: + platform: "youtube" + title: "Updated Video Title" + description: "New SEO-optimized description" + tags: ["seo", "marketing", "tutorial"] + direct-video-id: + summary: Update a video uploaded directly to YouTube + value: + platform: "youtube" + videoId: "dQw4w9WgXcQ" + accountId: "68fb37418bbca9c10cbfef26" + title: "Updated Title with SEO Keywords" + tags: ["seo", "youtube", "optimization"] responses: '200': description: Metadata updated successfully @@ -5422,12 +5446,13 @@ paths: properties: success: { type: boolean } message: { type: string } + videoId: { type: string, description: Only present in direct video ID mode } updatedFields: type: array items: { type: string } example: success: true - message: "Video metadata updated on youtube successfully" + message: "YouTube video metadata updated successfully" updatedFields: ["title", "description", "tags"] '400': description: "Invalid request: unsupported platform, post not published, missing fields, or validation error." diff --git a/pyproject.toml b/pyproject.toml index af891d8..f326b88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.27" +version = "1.3.28" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index cb6f1a9..9375d65 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.27" +__version__ = "1.3.28" __all__ = [ # Client diff --git a/src/late/mcp/generated_tools.py b/src/late/mcp/generated_tools.py index 7d3dffc..38bc1c6 100644 --- a/src/late/mcp/generated_tools.py +++ b/src/late/mcp/generated_tools.py @@ -13,35 +13,37 @@ def _format_response(response: Any) -> str: """Format SDK response for MCP output.""" if response is None: return "Success" - if hasattr(response, '__dict__'): + if hasattr(response, "__dict__"): # Handle response objects - if hasattr(response, 'posts') and response.posts: + if hasattr(response, "posts") and response.posts: posts = response.posts lines = [f"Found {len(posts)} post(s):"] for p in posts[:10]: - content = str(getattr(p, 'content', ''))[:50] - status = getattr(p, 'status', 'unknown') + content = str(getattr(p, "content", ""))[:50] + status = getattr(p, "status", "unknown") lines.append(f"- [{status}] {content}...") return "\n".join(lines) - if hasattr(response, 'accounts') and response.accounts: + if hasattr(response, "accounts") and response.accounts: accs = response.accounts lines = [f"Found {len(accs)} account(s):"] for a in accs[:10]: - platform = getattr(a, 'platform', '?') - username = getattr(a, 'username', None) or getattr(a, 'displayName', '?') + platform = getattr(a, "platform", "?") + username = getattr(a, "username", None) or getattr( + a, "displayName", "?" + ) lines.append(f"- {platform}: {username}") return "\n".join(lines) - if hasattr(response, 'profiles') and response.profiles: + if hasattr(response, "profiles") and response.profiles: profiles = response.profiles lines = [f"Found {len(profiles)} profile(s):"] for p in profiles[:10]: - name = getattr(p, 'name', 'Unnamed') + name = getattr(p, "name", "Unnamed") lines.append(f"- {name}") return "\n".join(lines) - if hasattr(response, 'post') and response.post: + if hasattr(response, "post") and response.post: p = response.post return f"Post ID: {getattr(p, 'field_id', 'N/A')}\nStatus: {getattr(p, 'status', 'N/A')}" - if hasattr(response, 'profile') and response.profile: + if hasattr(response, "profile") and response.profile: p = response.profile return f"Profile: {getattr(p, 'name', 'N/A')} (ID: {getattr(p, 'field_id', 'N/A')})" return str(response) @@ -52,7 +54,6 @@ def register_generated_tools(mcp, _get_client): # ACCOUNT_GROUPS - @mcp.tool() def account_groups_list_account_groups() -> str: """List groups""" @@ -61,8 +62,7 @@ def account_groups_list_account_groups() -> str: response = client.account_groups.list_account_groups() return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_groups_create_account_group(name: str, account_ids: str) -> str: @@ -73,14 +73,17 @@ def account_groups_create_account_group(name: str, account_ids: str) -> str: account_ids: (required)""" client = _get_client() try: - response = client.account_groups.create_account_group(name=name, account_ids=account_ids) + response = client.account_groups.create_account_group( + name=name, account_ids=account_ids + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def account_groups_update_account_group(group_id: str, name: str = "", account_ids: str = "") -> str: + def account_groups_update_account_group( + group_id: str, name: str = "", account_ids: str = "" + ) -> str: """Update group Args: @@ -89,11 +92,12 @@ def account_groups_update_account_group(group_id: str, name: str = "", account_i account_ids""" client = _get_client() try: - response = client.account_groups.update_account_group(group_id=group_id, name=name, account_ids=account_ids) + response = client.account_groups.update_account_group( + group_id=group_id, name=name, account_ids=account_ids + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_groups_delete_account_group(group_id: str) -> str: @@ -106,11 +110,10 @@ def account_groups_delete_account_group(group_id: str) -> str: response = client.account_groups.delete_account_group(group_id=group_id) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # ACCOUNT_SETTINGS - @mcp.tool() def account_settings_get_messenger_menu(account_id: str) -> str: """Get FB persistent menu @@ -122,11 +125,12 @@ def account_settings_get_messenger_menu(account_id: str) -> str: response = client.account_settings.get_messenger_menu(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def account_settings_set_messenger_menu(account_id: str, persistent_menu: str) -> str: + def account_settings_set_messenger_menu( + account_id: str, persistent_menu: str + ) -> str: """Set FB persistent menu Args: @@ -134,11 +138,12 @@ def account_settings_set_messenger_menu(account_id: str, persistent_menu: str) - persistent_menu: Persistent menu configuration array (Meta format) (required)""" client = _get_client() try: - response = client.account_settings.set_messenger_menu(account_id=account_id, persistent_menu=persistent_menu) + response = client.account_settings.set_messenger_menu( + account_id=account_id, persistent_menu=persistent_menu + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_delete_messenger_menu(account_id: str) -> str: @@ -148,11 +153,12 @@ def account_settings_delete_messenger_menu(account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.account_settings.delete_messenger_menu(account_id=account_id) + response = client.account_settings.delete_messenger_menu( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_get_instagram_ice_breakers(account_id: str) -> str: @@ -162,14 +168,17 @@ def account_settings_get_instagram_ice_breakers(account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.account_settings.get_instagram_ice_breakers(account_id=account_id) + response = client.account_settings.get_instagram_ice_breakers( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def account_settings_set_instagram_ice_breakers(account_id: str, ice_breakers: str) -> str: + def account_settings_set_instagram_ice_breakers( + account_id: str, ice_breakers: str + ) -> str: """Set IG ice breakers Args: @@ -177,11 +186,12 @@ def account_settings_set_instagram_ice_breakers(account_id: str, ice_breakers: s ice_breakers: (required)""" client = _get_client() try: - response = client.account_settings.set_instagram_ice_breakers(account_id=account_id, ice_breakers=ice_breakers) + response = client.account_settings.set_instagram_ice_breakers( + account_id=account_id, ice_breakers=ice_breakers + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_delete_instagram_ice_breakers(account_id: str) -> str: @@ -191,11 +201,12 @@ def account_settings_delete_instagram_ice_breakers(account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.account_settings.delete_instagram_ice_breakers(account_id=account_id) + response = client.account_settings.delete_instagram_ice_breakers( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_get_telegram_commands(account_id: str) -> str: @@ -205,11 +216,12 @@ def account_settings_get_telegram_commands(account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.account_settings.get_telegram_commands(account_id=account_id) + response = client.account_settings.get_telegram_commands( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_set_telegram_commands(account_id: str, commands: str) -> str: @@ -220,11 +232,12 @@ def account_settings_set_telegram_commands(account_id: str, commands: str) -> st commands: (required)""" client = _get_client() try: - response = client.account_settings.set_telegram_commands(account_id=account_id, commands=commands) + response = client.account_settings.set_telegram_commands( + account_id=account_id, commands=commands + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def account_settings_delete_telegram_commands(account_id: str) -> str: @@ -234,16 +247,19 @@ def account_settings_delete_telegram_commands(account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.account_settings.delete_telegram_commands(account_id=account_id) + response = client.account_settings.delete_telegram_commands( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # ACCOUNTS - @mcp.tool() - def accounts_list_accounts(profile_id: str = "", platform: str = "", include_over_limit: bool = False) -> str: + def accounts_list_accounts( + profile_id: str = "", platform: str = "", include_over_limit: bool = False + ) -> str: """List accounts Args: @@ -252,14 +268,23 @@ def accounts_list_accounts(profile_id: str = "", platform: str = "", include_ove include_over_limit: When true, includes accounts from over-limit profiles.""" client = _get_client() try: - response = client.accounts.list_accounts(profile_id=profile_id, platform=platform, include_over_limit=include_over_limit) + response = client.accounts.list_accounts( + profile_id=profile_id, + platform=platform, + include_over_limit=include_over_limit, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_follower_stats(account_ids: str = "", profile_id: str = "", from_date: str = "", to_date: str = "", granularity: str = "daily") -> str: + def accounts_get_follower_stats( + account_ids: str = "", + profile_id: str = "", + from_date: str = "", + to_date: str = "", + granularity: str = "daily", + ) -> str: """Get follower stats Args: @@ -270,14 +295,21 @@ def accounts_get_follower_stats(account_ids: str = "", profile_id: str = "", fro granularity: Data aggregation level""" client = _get_client() try: - response = client.accounts.get_follower_stats(account_ids=account_ids, profile_id=profile_id, from_date=from_date, to_date=to_date, granularity=granularity) + response = client.accounts.get_follower_stats( + account_ids=account_ids, + profile_id=profile_id, + from_date=from_date, + to_date=to_date, + granularity=granularity, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_update_account(account_id: str, username: str = "", display_name: str = "") -> str: + def accounts_update_account( + account_id: str, username: str = "", display_name: str = "" + ) -> str: """Update account Args: @@ -286,11 +318,12 @@ def accounts_update_account(account_id: str, username: str = "", display_name: s display_name""" client = _get_client() try: - response = client.accounts.update_account(account_id=account_id, username=username, display_name=display_name) + response = client.accounts.update_account( + account_id=account_id, username=username, display_name=display_name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def accounts_delete_account(account_id: str) -> str: @@ -303,11 +336,12 @@ def accounts_delete_account(account_id: str) -> str: response = client.accounts.delete_account(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_all_accounts_health(profile_id: str = "", platform: str = "", status: str = "") -> str: + def accounts_get_all_accounts_health( + profile_id: str = "", platform: str = "", status: str = "" + ) -> str: """Check accounts health Args: @@ -316,11 +350,12 @@ def accounts_get_all_accounts_health(profile_id: str = "", platform: str = "", s status: Filter by health status""" client = _get_client() try: - response = client.accounts.get_all_accounts_health(profile_id=profile_id, platform=platform, status=status) + response = client.accounts.get_all_accounts_health( + profile_id=profile_id, platform=platform, status=status + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def accounts_get_account_health(account_id: str) -> str: @@ -333,11 +368,12 @@ def accounts_get_account_health(account_id: str) -> str: response = client.accounts.get_account_health(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_tik_tok_creator_info(account_id: str, media_type: str = "video") -> str: + def accounts_get_tik_tok_creator_info( + account_id: str, media_type: str = "video" + ) -> str: """Get TikTok creator info Args: @@ -345,14 +381,20 @@ def accounts_get_tik_tok_creator_info(account_id: str, media_type: str = "video" media_type: The media type to get creator info for (affects available interaction settings)""" client = _get_client() try: - response = client.accounts.get_tik_tok_creator_info(account_id=account_id, media_type=media_type) + response = client.accounts.get_tik_tok_creator_info( + account_id=account_id, media_type=media_type + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_google_business_reviews(account_id: str, location_id: str = "", page_size: int = 50, page_token: str = "") -> str: + def accounts_get_google_business_reviews( + account_id: str, + location_id: str = "", + page_size: int = 50, + page_token: str = "", + ) -> str: """Get reviews Args: @@ -362,14 +404,20 @@ def accounts_get_google_business_reviews(account_id: str, location_id: str = "", page_token: Pagination token from previous response""" client = _get_client() try: - response = client.accounts.get_google_business_reviews(account_id=account_id, location_id=location_id, page_size=page_size, page_token=page_token) + response = client.accounts.get_google_business_reviews( + account_id=account_id, + location_id=location_id, + page_size=page_size, + page_token=page_token, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_google_business_food_menus(account_id: str, location_id: str = "") -> str: + def accounts_get_google_business_food_menus( + account_id: str, location_id: str = "" + ) -> str: """Get food menus Args: @@ -377,14 +425,17 @@ def accounts_get_google_business_food_menus(account_id: str, location_id: str = location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.""" client = _get_client() try: - response = client.accounts.get_google_business_food_menus(account_id=account_id, location_id=location_id) + response = client.accounts.get_google_business_food_menus( + account_id=account_id, location_id=location_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_update_google_business_food_menus(account_id: str, menus: str, location_id: str = "", update_mask: str = "") -> str: + def accounts_update_google_business_food_menus( + account_id: str, menus: str, location_id: str = "", update_mask: str = "" + ) -> str: """Update food menus Args: @@ -394,14 +445,20 @@ def accounts_update_google_business_food_menus(account_id: str, menus: str, loca update_mask: Field mask for partial updates (e.g. "menus")""" client = _get_client() try: - response = client.accounts.update_google_business_food_menus(account_id=account_id, location_id=location_id, menus=menus, update_mask=update_mask) + response = client.accounts.update_google_business_food_menus( + account_id=account_id, + location_id=location_id, + menus=menus, + update_mask=update_mask, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_google_business_location_details(account_id: str, location_id: str = "", read_mask: str = "") -> str: + def accounts_get_google_business_location_details( + account_id: str, location_id: str = "", read_mask: str = "" + ) -> str: """Get location details Args: @@ -410,14 +467,26 @@ def accounts_get_google_business_location_details(account_id: str, location_id: read_mask: Comma-separated fields to return. Available: name, title, phoneNumbers, categories, storefrontAddress, websiteUri, regularHours, specialHours, serviceArea, serviceItems, profile, openInfo, metadata, moreHours.""" client = _get_client() try: - response = client.accounts.get_google_business_location_details(account_id=account_id, location_id=location_id, read_mask=read_mask) + response = client.accounts.get_google_business_location_details( + account_id=account_id, location_id=location_id, read_mask=read_mask + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_update_google_business_location_details(account_id: str, update_mask: str, location_id: str = "", regular_hours: str = "", special_hours: str = "", profile: str = "", website_uri: str = "", phone_numbers: str = "", categories: str = "", service_items: str = "") -> str: + def accounts_update_google_business_location_details( + account_id: str, + update_mask: str, + location_id: str = "", + regular_hours: str = "", + special_hours: str = "", + profile: str = "", + website_uri: str = "", + phone_numbers: str = "", + categories: str = "", + service_items: str = "", + ) -> str: """Update location details Args: @@ -433,14 +502,29 @@ def accounts_update_google_business_location_details(account_id: str, update_mas service_items: Services offered by the business. Use updateMask='serviceItems' to update.""" client = _get_client() try: - response = client.accounts.update_google_business_location_details(account_id=account_id, location_id=location_id, update_mask=update_mask, regular_hours=regular_hours, special_hours=special_hours, profile=profile, website_uri=website_uri, phone_numbers=phone_numbers, categories=categories, service_items=service_items) + response = client.accounts.update_google_business_location_details( + account_id=account_id, + location_id=location_id, + update_mask=update_mask, + regular_hours=regular_hours, + special_hours=special_hours, + profile=profile, + website_uri=website_uri, + phone_numbers=phone_numbers, + categories=categories, + service_items=service_items, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_list_google_business_media(account_id: str, location_id: str = "", page_size: int = 100, page_token: str = "") -> str: + def accounts_list_google_business_media( + account_id: str, + location_id: str = "", + page_size: int = 100, + page_token: str = "", + ) -> str: """List media Args: @@ -450,14 +534,25 @@ def accounts_list_google_business_media(account_id: str, location_id: str = "", page_token: Pagination token from previous response""" client = _get_client() try: - response = client.accounts.list_google_business_media(account_id=account_id, location_id=location_id, page_size=page_size, page_token=page_token) + response = client.accounts.list_google_business_media( + account_id=account_id, + location_id=location_id, + page_size=page_size, + page_token=page_token, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_create_google_business_media(account_id: str, source_url: str, location_id: str = "", media_format: str = "PHOTO", description: str = "", category: str = "") -> str: + def accounts_create_google_business_media( + account_id: str, + source_url: str, + location_id: str = "", + media_format: str = "PHOTO", + description: str = "", + category: str = "", + ) -> str: """Upload photo Args: @@ -469,14 +564,22 @@ def accounts_create_google_business_media(account_id: str, source_url: str, loca category: Where the photo appears on the listing""" client = _get_client() try: - response = client.accounts.create_google_business_media(account_id=account_id, location_id=location_id, source_url=source_url, media_format=media_format, description=description, category=category) + response = client.accounts.create_google_business_media( + account_id=account_id, + location_id=location_id, + source_url=source_url, + media_format=media_format, + description=description, + category=category, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_delete_google_business_media(account_id: str, media_id: str, location_id: str = "") -> str: + def accounts_delete_google_business_media( + account_id: str, media_id: str, location_id: str = "" + ) -> str: """Delete photo Args: @@ -485,14 +588,17 @@ def accounts_delete_google_business_media(account_id: str, media_id: str, locati media_id: The media item ID to delete (required)""" client = _get_client() try: - response = client.accounts.delete_google_business_media(account_id=account_id, location_id=location_id, media_id=media_id) + response = client.accounts.delete_google_business_media( + account_id=account_id, location_id=location_id, media_id=media_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_google_business_attributes(account_id: str, location_id: str = "") -> str: + def accounts_get_google_business_attributes( + account_id: str, location_id: str = "" + ) -> str: """Get attributes Args: @@ -500,14 +606,17 @@ def accounts_get_google_business_attributes(account_id: str, location_id: str = location_id: Override which location to query. If omitted, uses the account's selected location. Use GET /gmb-locations to list valid IDs.""" client = _get_client() try: - response = client.accounts.get_google_business_attributes(account_id=account_id, location_id=location_id) + response = client.accounts.get_google_business_attributes( + account_id=account_id, location_id=location_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_update_google_business_attributes(account_id: str, attributes: str, attribute_mask: str, location_id: str = "") -> str: + def accounts_update_google_business_attributes( + account_id: str, attributes: str, attribute_mask: str, location_id: str = "" + ) -> str: """Update attributes Args: @@ -517,14 +626,23 @@ def accounts_update_google_business_attributes(account_id: str, attributes: str, attribute_mask: Comma-separated attribute names to update (e.g. 'has_delivery,has_takeout') (required)""" client = _get_client() try: - response = client.accounts.update_google_business_attributes(account_id=account_id, location_id=location_id, attributes=attributes, attribute_mask=attribute_mask) + response = client.accounts.update_google_business_attributes( + account_id=account_id, + location_id=location_id, + attributes=attributes, + attribute_mask=attribute_mask, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_list_google_business_place_actions(account_id: str, location_id: str = "", page_size: int = 100, page_token: str = "") -> str: + def accounts_list_google_business_place_actions( + account_id: str, + location_id: str = "", + page_size: int = 100, + page_token: str = "", + ) -> str: """List action links Args: @@ -534,14 +652,20 @@ def accounts_list_google_business_place_actions(account_id: str, location_id: st page_token""" client = _get_client() try: - response = client.accounts.list_google_business_place_actions(account_id=account_id, location_id=location_id, page_size=page_size, page_token=page_token) + response = client.accounts.list_google_business_place_actions( + account_id=account_id, + location_id=location_id, + page_size=page_size, + page_token=page_token, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_create_google_business_place_action(account_id: str, uri: str, place_action_type: str, location_id: str = "") -> str: + def accounts_create_google_business_place_action( + account_id: str, uri: str, place_action_type: str, location_id: str = "" + ) -> str: """Create action link Args: @@ -551,14 +675,20 @@ def accounts_create_google_business_place_action(account_id: str, uri: str, plac place_action_type: Type of action (required)""" client = _get_client() try: - response = client.accounts.create_google_business_place_action(account_id=account_id, location_id=location_id, uri=uri, place_action_type=place_action_type) + response = client.accounts.create_google_business_place_action( + account_id=account_id, + location_id=location_id, + uri=uri, + place_action_type=place_action_type, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_delete_google_business_place_action(account_id: str, name: str, location_id: str = "") -> str: + def accounts_delete_google_business_place_action( + account_id: str, name: str, location_id: str = "" + ) -> str: """Delete action link Args: @@ -567,14 +697,17 @@ def accounts_delete_google_business_place_action(account_id: str, name: str, loc name: The resource name of the place action link (e.g. locations/123/placeActionLinks/456) (required)""" client = _get_client() try: - response = client.accounts.delete_google_business_place_action(account_id=account_id, location_id=location_id, name=name) + response = client.accounts.delete_google_business_place_action( + account_id=account_id, location_id=location_id, name=name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def accounts_get_linked_in_mentions(account_id: str, url: str, display_name: str = "") -> str: + def accounts_get_linked_in_mentions( + account_id: str, url: str, display_name: str = "" + ) -> str: """Resolve LinkedIn mention Args: @@ -583,16 +716,29 @@ def accounts_get_linked_in_mentions(account_id: str, url: str, display_name: str display_name: Exact display name as shown on LinkedIn. Required for person mentions to be clickable. Optional for org mentions.""" client = _get_client() try: - response = client.accounts.get_linked_in_mentions(account_id=account_id, url=url, display_name=display_name) + response = client.accounts.get_linked_in_mentions( + account_id=account_id, url=url, display_name=display_name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # ANALYTICS - @mcp.tool() - def analytics_get_analytics(post_id: str = "", platform: str = "", profile_id: str = "", account_id: str = "", source: str = "all", from_date: str = "", to_date: str = "", limit: int = 50, page: int = 1, sort_by: str = "date", order: str = "desc") -> str: + def analytics_get_analytics( + post_id: str = "", + platform: str = "", + profile_id: str = "", + account_id: str = "", + source: str = "all", + from_date: str = "", + to_date: str = "", + limit: int = 50, + page: int = 1, + sort_by: str = "date", + order: str = "desc", + ) -> str: """Get post analytics Args: @@ -609,14 +755,27 @@ def analytics_get_analytics(post_id: str = "", platform: str = "", profile_id: s order: Sort order""" client = _get_client() try: - response = client.analytics.get_analytics(post_id=post_id, platform=platform, profile_id=profile_id, account_id=account_id, source=source, from_date=from_date, to_date=to_date, limit=limit, page=page, sort_by=sort_by, order=order) + response = client.analytics.get_analytics( + post_id=post_id, + platform=platform, + profile_id=profile_id, + account_id=account_id, + source=source, + from_date=from_date, + to_date=to_date, + limit=limit, + page=page, + sort_by=sort_by, + order=order, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_you_tube_daily_views(video_id: str, account_id: str, start_date: str = "", end_date: str = "") -> str: + def analytics_get_you_tube_daily_views( + video_id: str, account_id: str, start_date: str = "", end_date: str = "" + ) -> str: """Get YouTube daily views Args: @@ -626,56 +785,89 @@ def analytics_get_you_tube_daily_views(video_id: str, account_id: str, start_dat end_date: End date (YYYY-MM-DD). Defaults to 3 days ago (YouTube data latency).""" client = _get_client() try: - response = client.analytics.get_you_tube_daily_views(video_id=video_id, account_id=account_id, start_date=start_date, end_date=end_date) + response = client.analytics.get_you_tube_daily_views( + video_id=video_id, + account_id=account_id, + start_date=start_date, + end_date=end_date, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_instagram_account_insights(account_id: str, metrics: str = "", since: str = "", until: str = "", metric_type: str = "total_value", breakdown: str = "") -> str: + def analytics_get_instagram_account_insights( + account_id: str, + metrics: str = "", + since: str = "", + until: str = "", + metric_type: str = "total_value", + breakdown: str = "", + ) -> str: """Get Instagram account-level insights - Args: - account_id: The Zernio SocialAccount ID for the Instagram account (required) - metrics: Comma-separated list of metrics. Defaults to "reach,views,accounts_engaged,total_interactions". - Valid metrics: reach, views, accounts_engaged, total_interactions, comments, likes, saves, shares, - replies, reposts, follows_and_unfollows, profile_links_taps. - Note: only "reach" supports metricType=time_series. All other metrics are total_value only. - since: Start date (YYYY-MM-DD). Defaults to 30 days ago. - until: End date (YYYY-MM-DD). Defaults to today. - metric_type: "total_value" (default) returns aggregated totals and supports breakdowns. - "time_series" returns daily values but only works with the "reach" metric. - breakdown: Breakdown dimension (only valid with metricType=total_value). - Valid values depend on the metric: media_product_type, follow_type, follower_type, contact_button_type.""" - client = _get_client() - try: - response = client.analytics.get_instagram_account_insights(account_id=account_id, metrics=metrics, since=since, until=until, metric_type=metric_type, breakdown=breakdown) - return _format_response(response) - except Exception as e: - return f'Error: {e}' - - - @mcp.tool() - def analytics_get_instagram_demographics(account_id: str, metric: str = "follower_demographics", breakdown: str = "", timeframe: str = "this_month") -> str: + Args: + account_id: The Zernio SocialAccount ID for the Instagram account (required) + metrics: Comma-separated list of metrics. Defaults to "reach,views,accounts_engaged,total_interactions". + Valid metrics: reach, views, accounts_engaged, total_interactions, comments, likes, saves, shares, + replies, reposts, follows_and_unfollows, profile_links_taps. + Note: only "reach" supports metricType=time_series. All other metrics are total_value only. + since: Start date (YYYY-MM-DD). Defaults to 30 days ago. + until: End date (YYYY-MM-DD). Defaults to today. + metric_type: "total_value" (default) returns aggregated totals and supports breakdowns. + "time_series" returns daily values but only works with the "reach" metric. + breakdown: Breakdown dimension (only valid with metricType=total_value). + Valid values depend on the metric: media_product_type, follow_type, follower_type, contact_button_type.""" + client = _get_client() + try: + response = client.analytics.get_instagram_account_insights( + account_id=account_id, + metrics=metrics, + since=since, + until=until, + metric_type=metric_type, + breakdown=breakdown, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def analytics_get_instagram_demographics( + account_id: str, + metric: str = "follower_demographics", + breakdown: str = "", + timeframe: str = "this_month", + ) -> str: """Get Instagram audience demographics - Args: - account_id: The Zernio SocialAccount ID for the Instagram account (required) - metric: "follower_demographics" for follower audience data, or "engaged_audience_demographics" for engaged viewers. - breakdown: Comma-separated list of demographic dimensions: age, city, country, gender. - Defaults to all four if omitted. - timeframe: Time period for demographic data. Defaults to "this_month".""" + Args: + account_id: The Zernio SocialAccount ID for the Instagram account (required) + metric: "follower_demographics" for follower audience data, or "engaged_audience_demographics" for engaged viewers. + breakdown: Comma-separated list of demographic dimensions: age, city, country, gender. + Defaults to all four if omitted. + timeframe: Time period for demographic data. Defaults to "this_month".""" client = _get_client() try: - response = client.analytics.get_instagram_demographics(account_id=account_id, metric=metric, breakdown=breakdown, timeframe=timeframe) + response = client.analytics.get_instagram_demographics( + account_id=account_id, + metric=metric, + breakdown=breakdown, + timeframe=timeframe, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_daily_metrics(platform: str = "", profile_id: str = "", account_id: str = "", from_date: str = "", to_date: str = "", source: str = "all") -> str: + def analytics_get_daily_metrics( + platform: str = "", + profile_id: str = "", + account_id: str = "", + from_date: str = "", + to_date: str = "", + source: str = "all", + ) -> str: """Get daily aggregated metrics Args: @@ -687,14 +879,22 @@ def analytics_get_daily_metrics(platform: str = "", profile_id: str = "", accoun source: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.""" client = _get_client() try: - response = client.analytics.get_daily_metrics(platform=platform, profile_id=profile_id, account_id=account_id, from_date=from_date, to_date=to_date, source=source) + response = client.analytics.get_daily_metrics( + platform=platform, + profile_id=profile_id, + account_id=account_id, + from_date=from_date, + to_date=to_date, + source=source, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_best_time_to_post(platform: str = "", profile_id: str = "", source: str = "all") -> str: + def analytics_get_best_time_to_post( + platform: str = "", profile_id: str = "", source: str = "all" + ) -> str: """Get best times to post Args: @@ -703,14 +903,17 @@ def analytics_get_best_time_to_post(platform: str = "", profile_id: str = "", so source: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.""" client = _get_client() try: - response = client.analytics.get_best_time_to_post(platform=platform, profile_id=profile_id, source=source) + response = client.analytics.get_best_time_to_post( + platform=platform, profile_id=profile_id, source=source + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_content_decay(platform: str = "", profile_id: str = "", source: str = "all") -> str: + def analytics_get_content_decay( + platform: str = "", profile_id: str = "", source: str = "all" + ) -> str: """Get content performance decay Args: @@ -719,14 +922,17 @@ def analytics_get_content_decay(platform: str = "", profile_id: str = "", source source: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.""" client = _get_client() try: - response = client.analytics.get_content_decay(platform=platform, profile_id=profile_id, source=source) + response = client.analytics.get_content_decay( + platform=platform, profile_id=profile_id, source=source + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_posting_frequency(platform: str = "", profile_id: str = "", source: str = "all") -> str: + def analytics_get_posting_frequency( + platform: str = "", profile_id: str = "", source: str = "all" + ) -> str: """Get posting frequency vs engagement Args: @@ -735,31 +941,41 @@ def analytics_get_posting_frequency(platform: str = "", profile_id: str = "", so source: Filter by post origin. "late" for posts published via Zernio, "external" for posts imported from platforms.""" client = _get_client() try: - response = client.analytics.get_posting_frequency(platform=platform, profile_id=profile_id, source=source) + response = client.analytics.get_posting_frequency( + platform=platform, profile_id=profile_id, source=source + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_post_timeline(post_id: str, from_date: str = "", to_date: str = "") -> str: + def analytics_get_post_timeline( + post_id: str, from_date: str = "", to_date: str = "" + ) -> str: """Get post analytics timeline - Args: - post_id: The post to fetch timeline for. Accepts an ExternalPost ID, a platformPostId, or a Zernio Post ID. - (required) - from_date: Start of date range (ISO 8601). Defaults to 90 days ago. - to_date: End of date range (ISO 8601). Defaults to now.""" + Args: + post_id: The post to fetch timeline for. Accepts an ExternalPost ID, a platformPostId, or a Zernio Post ID. + (required) + from_date: Start of date range (ISO 8601). Defaults to 90 days ago. + to_date: End of date range (ISO 8601). Defaults to now.""" client = _get_client() try: - response = client.analytics.get_post_timeline(post_id=post_id, from_date=from_date, to_date=to_date) + response = client.analytics.get_post_timeline( + post_id=post_id, from_date=from_date, to_date=to_date + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_linked_in_aggregate_analytics(account_id: str, aggregation: str = "TOTAL", start_date: str = "", end_date: str = "", metrics: str = "") -> str: + def analytics_get_linked_in_aggregate_analytics( + account_id: str, + aggregation: str = "TOTAL", + start_date: str = "", + end_date: str = "", + metrics: str = "", + ) -> str: """Get LinkedIn aggregate stats Args: @@ -770,11 +986,16 @@ def analytics_get_linked_in_aggregate_analytics(account_id: str, aggregation: st metrics: Comma-separated metrics: IMPRESSION, MEMBERS_REACHED, REACTION, COMMENT, RESHARE. Omit for all.""" client = _get_client() try: - response = client.analytics.get_linked_in_aggregate_analytics(account_id=account_id, aggregation=aggregation, start_date=start_date, end_date=end_date, metrics=metrics) + response = client.analytics.get_linked_in_aggregate_analytics( + account_id=account_id, + aggregation=aggregation, + start_date=start_date, + end_date=end_date, + metrics=metrics, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def analytics_get_linked_in_post_analytics(account_id: str, urn: str) -> str: @@ -785,14 +1006,17 @@ def analytics_get_linked_in_post_analytics(account_id: str, urn: str) -> str: urn: The LinkedIn post URN (required)""" client = _get_client() try: - response = client.analytics.get_linked_in_post_analytics(account_id=account_id, urn=urn) + response = client.analytics.get_linked_in_post_analytics( + account_id=account_id, urn=urn + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def analytics_get_linked_in_post_reactions(account_id: str, urn: str, limit: int = 25, cursor: str = "") -> str: + def analytics_get_linked_in_post_reactions( + account_id: str, urn: str, limit: int = 25, cursor: str = "" + ) -> str: """Get LinkedIn post reactions Args: @@ -802,14 +1026,15 @@ def analytics_get_linked_in_post_reactions(account_id: str, urn: str, limit: int cursor: Offset-based pagination start index""" client = _get_client() try: - response = client.analytics.get_linked_in_post_reactions(account_id=account_id, urn=urn, limit=limit, cursor=cursor) + response = client.analytics.get_linked_in_post_reactions( + account_id=account_id, urn=urn, limit=limit, cursor=cursor + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # API_KEYS - @mcp.tool() def api_keys_list_api_keys() -> str: """List keys""" @@ -818,11 +1043,16 @@ def api_keys_list_api_keys() -> str: response = client.api_keys.list_api_keys() return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def api_keys_create_api_key(name: str, expires_in: int = 0, scope: str = "full", profile_ids: str = "", permission: str = "read-write") -> str: + def api_keys_create_api_key( + name: str, + expires_in: int = 0, + scope: str = "full", + profile_ids: str = "", + permission: str = "read-write", + ) -> str: """Create key Args: @@ -833,11 +1063,16 @@ def api_keys_create_api_key(name: str, expires_in: int = 0, scope: str = "full", permission: 'read-write' allows all operations (default), 'read' restricts to GET requests only""" client = _get_client() try: - response = client.api_keys.create_api_key(name=name, expires_in=expires_in, scope=scope, profile_ids=profile_ids, permission=permission) + response = client.api_keys.create_api_key( + name=name, + expires_in=expires_in, + scope=scope, + profile_ids=profile_ids, + permission=permission, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def api_keys_delete_api_key(key_id: str) -> str: @@ -850,13 +1085,18 @@ def api_keys_delete_api_key(key_id: str) -> str: response = client.api_keys.delete_api_key(key_id=key_id) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # BROADCASTS - @mcp.tool() - def broadcasts_list_broadcasts(profile_id: str = "", status: str = "", platform: str = "", limit: int = 50, skip: int = 0) -> str: + def broadcasts_list_broadcasts( + profile_id: str = "", + status: str = "", + platform: str = "", + limit: int = 50, + skip: int = 0, + ) -> str: """List broadcasts Args: @@ -867,14 +1107,28 @@ def broadcasts_list_broadcasts(profile_id: str = "", status: str = "", platform: skip""" client = _get_client() try: - response = client.broadcasts.list_broadcasts(profile_id=profile_id, status=status, platform=platform, limit=limit, skip=skip) + response = client.broadcasts.list_broadcasts( + profile_id=profile_id, + status=status, + platform=platform, + limit=limit, + skip=skip, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def broadcasts_create_broadcast(profile_id: str, account_id: str, platform: str, name: str, description: str = "", message: str = "", template: str = "", segment_filters: str = "") -> str: + def broadcasts_create_broadcast( + profile_id: str, + account_id: str, + platform: str, + name: str, + description: str = "", + message: str = "", + template: str = "", + segment_filters: str = "", + ) -> str: """Create a broadcast draft Args: @@ -888,11 +1142,19 @@ def broadcasts_create_broadcast(profile_id: str, account_id: str, platform: str, segment_filters""" client = _get_client() try: - response = client.broadcasts.create_broadcast(profile_id=profile_id, account_id=account_id, platform=platform, name=name, description=description, message=message, template=template, segment_filters=segment_filters) + response = client.broadcasts.create_broadcast( + profile_id=profile_id, + account_id=account_id, + platform=platform, + name=name, + description=description, + message=message, + template=template, + segment_filters=segment_filters, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_get_broadcast(broadcast_id: str) -> str: @@ -905,8 +1167,7 @@ def broadcasts_get_broadcast(broadcast_id: str) -> str: response = client.broadcasts.get_broadcast(broadcast_id=broadcast_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_update_broadcast(broadcast_id: str) -> str: @@ -919,8 +1180,7 @@ def broadcasts_update_broadcast(broadcast_id: str) -> str: response = client.broadcasts.update_broadcast(broadcast_id=broadcast_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_delete_broadcast(broadcast_id: str) -> str: @@ -933,8 +1193,7 @@ def broadcasts_delete_broadcast(broadcast_id: str) -> str: response = client.broadcasts.delete_broadcast(broadcast_id=broadcast_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_send_broadcast(broadcast_id: str) -> str: @@ -947,8 +1206,7 @@ def broadcasts_send_broadcast(broadcast_id: str) -> str: response = client.broadcasts.send_broadcast(broadcast_id=broadcast_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_schedule_broadcast(broadcast_id: str, scheduled_at: str) -> str: @@ -959,11 +1217,12 @@ def broadcasts_schedule_broadcast(broadcast_id: str, scheduled_at: str) -> str: scheduled_at: (required)""" client = _get_client() try: - response = client.broadcasts.schedule_broadcast(broadcast_id=broadcast_id, scheduled_at=scheduled_at) + response = client.broadcasts.schedule_broadcast( + broadcast_id=broadcast_id, scheduled_at=scheduled_at + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def broadcasts_cancel_broadcast(broadcast_id: str) -> str: @@ -976,11 +1235,12 @@ def broadcasts_cancel_broadcast(broadcast_id: str) -> str: response = client.broadcasts.cancel_broadcast(broadcast_id=broadcast_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def broadcasts_list_broadcast_recipients(broadcast_id: str, status: str = "", limit: int = 50, skip: int = 0) -> str: + def broadcasts_list_broadcast_recipients( + broadcast_id: str, status: str = "", limit: int = 50, skip: int = 0 + ) -> str: """List broadcast recipients Args: @@ -990,14 +1250,20 @@ def broadcasts_list_broadcast_recipients(broadcast_id: str, status: str = "", li skip""" client = _get_client() try: - response = client.broadcasts.list_broadcast_recipients(broadcast_id=broadcast_id, status=status, limit=limit, skip=skip) + response = client.broadcasts.list_broadcast_recipients( + broadcast_id=broadcast_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def broadcasts_add_broadcast_recipients(broadcast_id: str, contact_ids: str = "", phones: str = "", use_segment: bool = False) -> str: + def broadcasts_add_broadcast_recipients( + broadcast_id: str, + contact_ids: str = "", + phones: str = "", + use_segment: bool = False, + ) -> str: """Add recipients to a broadcast Args: @@ -1007,14 +1273,18 @@ def broadcasts_add_broadcast_recipients(broadcast_id: str, contact_ids: str = "" use_segment: Auto-populate from broadcast segment filters""" client = _get_client() try: - response = client.broadcasts.add_broadcast_recipients(broadcast_id=broadcast_id, contact_ids=contact_ids, phones=phones, use_segment=use_segment) + response = client.broadcasts.add_broadcast_recipients( + broadcast_id=broadcast_id, + contact_ids=contact_ids, + phones=phones, + use_segment=use_segment, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # COMMENT_AUTOMATIONS - @mcp.tool() def comment_automations_list_comment_automations(profile_id: str = "") -> str: """List comment-to-DM automations @@ -1023,14 +1293,26 @@ def comment_automations_list_comment_automations(profile_id: str = "") -> str: profile_id: Filter by profile. Omit to list across all profiles""" client = _get_client() try: - response = client.comment_automations.list_comment_automations(profile_id=profile_id) + response = client.comment_automations.list_comment_automations( + profile_id=profile_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comment_automations_create_comment_automation(profile_id: str, account_id: str, platform_post_id: str, name: str, dm_message: str, post_id: str = "", post_title: str = "", keywords: str = "", match_mode: str = "contains", comment_reply: str = "") -> str: + def comment_automations_create_comment_automation( + profile_id: str, + account_id: str, + platform_post_id: str, + name: str, + dm_message: str, + post_id: str = "", + post_title: str = "", + keywords: str = "", + match_mode: str = "contains", + comment_reply: str = "", + ) -> str: """Create a comment-to-DM automation Args: @@ -1046,11 +1328,21 @@ def comment_automations_create_comment_automation(profile_id: str, account_id: s comment_reply: Optional public reply to the comment""" client = _get_client() try: - response = client.comment_automations.create_comment_automation(profile_id=profile_id, account_id=account_id, platform_post_id=platform_post_id, post_id=post_id, post_title=post_title, name=name, keywords=keywords, match_mode=match_mode, dm_message=dm_message, comment_reply=comment_reply) + response = client.comment_automations.create_comment_automation( + profile_id=profile_id, + account_id=account_id, + platform_post_id=platform_post_id, + post_id=post_id, + post_title=post_title, + name=name, + keywords=keywords, + match_mode=match_mode, + dm_message=dm_message, + comment_reply=comment_reply, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def comment_automations_get_comment_automation(automation_id: str) -> str: @@ -1060,14 +1352,23 @@ def comment_automations_get_comment_automation(automation_id: str) -> str: automation_id: (required)""" client = _get_client() try: - response = client.comment_automations.get_comment_automation(automation_id=automation_id) + response = client.comment_automations.get_comment_automation( + automation_id=automation_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comment_automations_update_comment_automation(automation_id: str, name: str = "", keywords: str = "", match_mode: str = "", dm_message: str = "", comment_reply: str = "", is_active: bool = False) -> str: + def comment_automations_update_comment_automation( + automation_id: str, + name: str = "", + keywords: str = "", + match_mode: str = "", + dm_message: str = "", + comment_reply: str = "", + is_active: bool = False, + ) -> str: """Update automation settings Args: @@ -1080,11 +1381,18 @@ def comment_automations_update_comment_automation(automation_id: str, name: str is_active""" client = _get_client() try: - response = client.comment_automations.update_comment_automation(automation_id=automation_id, name=name, keywords=keywords, match_mode=match_mode, dm_message=dm_message, comment_reply=comment_reply, is_active=is_active) + response = client.comment_automations.update_comment_automation( + automation_id=automation_id, + name=name, + keywords=keywords, + match_mode=match_mode, + dm_message=dm_message, + comment_reply=comment_reply, + is_active=is_active, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def comment_automations_delete_comment_automation(automation_id: str) -> str: @@ -1094,14 +1402,17 @@ def comment_automations_delete_comment_automation(automation_id: str) -> str: automation_id: (required)""" client = _get_client() try: - response = client.comment_automations.delete_comment_automation(automation_id=automation_id) + response = client.comment_automations.delete_comment_automation( + automation_id=automation_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comment_automations_list_comment_automation_logs(automation_id: str, status: str = "", limit: int = 50, skip: int = 0) -> str: + def comment_automations_list_comment_automation_logs( + automation_id: str, status: str = "", limit: int = 50, skip: int = 0 + ) -> str: """List trigger logs for an automation Args: @@ -1111,16 +1422,27 @@ def comment_automations_list_comment_automation_logs(automation_id: str, status: skip""" client = _get_client() try: - response = client.comment_automations.list_comment_automation_logs(automation_id=automation_id, status=status, limit=limit, skip=skip) + response = client.comment_automations.list_comment_automation_logs( + automation_id=automation_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # COMMENTS - @mcp.tool() - def comments_list_inbox_comments(profile_id: str = "", platform: str = "", min_comments: int = 0, since: str = "", sort_by: str = "date", sort_order: str = "desc", limit: int = 50, cursor: str = "", account_id: str = "") -> str: + def comments_list_inbox_comments( + profile_id: str = "", + platform: str = "", + min_comments: int = 0, + since: str = "", + sort_by: str = "date", + sort_order: str = "desc", + limit: int = 50, + cursor: str = "", + account_id: str = "", + ) -> str: """List commented posts Args: @@ -1135,14 +1457,30 @@ def comments_list_inbox_comments(profile_id: str = "", platform: str = "", min_c account_id: Filter by specific social account ID""" client = _get_client() try: - response = client.comments.list_inbox_comments(profile_id=profile_id, platform=platform, min_comments=min_comments, since=since, sort_by=sort_by, sort_order=sort_order, limit=limit, cursor=cursor, account_id=account_id) + response = client.comments.list_inbox_comments( + profile_id=profile_id, + platform=platform, + min_comments=min_comments, + since=since, + sort_by=sort_by, + sort_order=sort_order, + limit=limit, + cursor=cursor, + account_id=account_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_get_inbox_post_comments(post_id: str, account_id: str, subreddit: str = "", limit: int = 25, cursor: str = "", comment_id: str = "") -> str: + def comments_get_inbox_post_comments( + post_id: str, + account_id: str, + subreddit: str = "", + limit: int = 25, + cursor: str = "", + comment_id: str = "", + ) -> str: """Get post comments Args: @@ -1154,14 +1492,28 @@ def comments_get_inbox_post_comments(post_id: str, account_id: str, subreddit: s comment_id: (Reddit only) Get replies to a specific comment""" client = _get_client() try: - response = client.comments.get_inbox_post_comments(post_id=post_id, account_id=account_id, subreddit=subreddit, limit=limit, cursor=cursor, comment_id=comment_id) + response = client.comments.get_inbox_post_comments( + post_id=post_id, + account_id=account_id, + subreddit=subreddit, + limit=limit, + cursor=cursor, + comment_id=comment_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_reply_to_inbox_post(post_id: str, account_id: str, message: str, comment_id: str = "", parent_cid: str = "", root_uri: str = "", root_cid: str = "") -> str: + def comments_reply_to_inbox_post( + post_id: str, + account_id: str, + message: str, + comment_id: str = "", + parent_cid: str = "", + root_uri: str = "", + root_cid: str = "", + ) -> str: """Reply to comment Args: @@ -1174,14 +1526,23 @@ def comments_reply_to_inbox_post(post_id: str, account_id: str, message: str, co root_cid: (Bluesky only) Root post CID""" client = _get_client() try: - response = client.comments.reply_to_inbox_post(post_id=post_id, account_id=account_id, message=message, comment_id=comment_id, parent_cid=parent_cid, root_uri=root_uri, root_cid=root_cid) + response = client.comments.reply_to_inbox_post( + post_id=post_id, + account_id=account_id, + message=message, + comment_id=comment_id, + parent_cid=parent_cid, + root_uri=root_uri, + root_cid=root_cid, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_delete_inbox_comment(post_id: str, account_id: str, comment_id: str) -> str: + def comments_delete_inbox_comment( + post_id: str, account_id: str, comment_id: str + ) -> str: """Delete comment Args: @@ -1190,14 +1551,17 @@ def comments_delete_inbox_comment(post_id: str, account_id: str, comment_id: str comment_id: (required)""" client = _get_client() try: - response = client.comments.delete_inbox_comment(post_id=post_id, account_id=account_id, comment_id=comment_id) + response = client.comments.delete_inbox_comment( + post_id=post_id, account_id=account_id, comment_id=comment_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_hide_inbox_comment(post_id: str, comment_id: str, account_id: str) -> str: + def comments_hide_inbox_comment( + post_id: str, comment_id: str, account_id: str + ) -> str: """Hide comment Args: @@ -1206,14 +1570,17 @@ def comments_hide_inbox_comment(post_id: str, comment_id: str, account_id: str) account_id: The social account ID (required)""" client = _get_client() try: - response = client.comments.hide_inbox_comment(post_id=post_id, comment_id=comment_id, account_id=account_id) + response = client.comments.hide_inbox_comment( + post_id=post_id, comment_id=comment_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_unhide_inbox_comment(post_id: str, comment_id: str, account_id: str) -> str: + def comments_unhide_inbox_comment( + post_id: str, comment_id: str, account_id: str + ) -> str: """Unhide comment Args: @@ -1222,14 +1589,17 @@ def comments_unhide_inbox_comment(post_id: str, comment_id: str, account_id: str account_id: (required)""" client = _get_client() try: - response = client.comments.unhide_inbox_comment(post_id=post_id, comment_id=comment_id, account_id=account_id) + response = client.comments.unhide_inbox_comment( + post_id=post_id, comment_id=comment_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_like_inbox_comment(post_id: str, comment_id: str, account_id: str, cid: str = "") -> str: + def comments_like_inbox_comment( + post_id: str, comment_id: str, account_id: str, cid: str = "" + ) -> str: """Like comment Args: @@ -1239,14 +1609,17 @@ def comments_like_inbox_comment(post_id: str, comment_id: str, account_id: str, cid: (Bluesky only) Content identifier for the comment""" client = _get_client() try: - response = client.comments.like_inbox_comment(post_id=post_id, comment_id=comment_id, account_id=account_id, cid=cid) + response = client.comments.like_inbox_comment( + post_id=post_id, comment_id=comment_id, account_id=account_id, cid=cid + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_unlike_inbox_comment(post_id: str, comment_id: str, account_id: str, like_uri: str = "") -> str: + def comments_unlike_inbox_comment( + post_id: str, comment_id: str, account_id: str, like_uri: str = "" + ) -> str: """Unlike comment Args: @@ -1256,14 +1629,20 @@ def comments_unlike_inbox_comment(post_id: str, comment_id: str, account_id: str like_uri: (Bluesky only) The like URI returned when liking""" client = _get_client() try: - response = client.comments.unlike_inbox_comment(post_id=post_id, comment_id=comment_id, account_id=account_id, like_uri=like_uri) + response = client.comments.unlike_inbox_comment( + post_id=post_id, + comment_id=comment_id, + account_id=account_id, + like_uri=like_uri, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def comments_send_private_reply_to_comment(post_id: str, comment_id: str, account_id: str, message: str) -> str: + def comments_send_private_reply_to_comment( + post_id: str, comment_id: str, account_id: str, message: str + ) -> str: """Send private reply Args: @@ -1273,16 +1652,22 @@ def comments_send_private_reply_to_comment(post_id: str, comment_id: str, accoun message: The message text to send as a private DM (required)""" client = _get_client() try: - response = client.comments.send_private_reply_to_comment(post_id=post_id, comment_id=comment_id, account_id=account_id, message=message) + response = client.comments.send_private_reply_to_comment( + post_id=post_id, + comment_id=comment_id, + account_id=account_id, + message=message, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # CONNECT - @mcp.tool() - def connect_get_connect_url(platform: str, profile_id: str, redirect_url: str = "", headless: bool = False) -> str: + def connect_get_connect_url( + platform: str, profile_id: str, redirect_url: str = "", headless: bool = False + ) -> str: """Get OAuth connect URL Args: @@ -1292,14 +1677,20 @@ def connect_get_connect_url(platform: str, profile_id: str, redirect_url: str = headless: When true, the user is redirected to your redirect_url with raw OAuth data (code, state) instead of Zernio's default account selection UI. Use this to build a custom connect experience.""" client = _get_client() try: - response = client.connect.get_connect_url(platform=platform, profile_id=profile_id, redirect_url=redirect_url, headless=headless) + response = client.connect.get_connect_url( + platform=platform, + profile_id=profile_id, + redirect_url=redirect_url, + headless=headless, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_handle_o_auth_callback(platform: str, code: str, state: str, profile_id: str) -> str: + def connect_handle_o_auth_callback( + platform: str, code: str, state: str, profile_id: str + ) -> str: """Complete OAuth callback Args: @@ -1309,11 +1700,12 @@ def connect_handle_o_auth_callback(platform: str, code: str, state: str, profile profile_id: (required)""" client = _get_client() try: - response = client.connect.handle_o_auth_callback(platform=platform, code=code, state=state, profile_id=profile_id) + response = client.connect.handle_o_auth_callback( + platform=platform, code=code, state=state, profile_id=profile_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_list_facebook_pages(profile_id: str, temp_token: str) -> str: @@ -1324,14 +1716,21 @@ def connect_list_facebook_pages(profile_id: str, temp_token: str) -> str: temp_token: Temporary Facebook access token from the OAuth callback redirect (required)""" client = _get_client() try: - response = client.connect.list_facebook_pages(profile_id=profile_id, temp_token=temp_token) + response = client.connect.list_facebook_pages( + profile_id=profile_id, temp_token=temp_token + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_select_facebook_page(profile_id: str, page_id: str, temp_token: str, user_profile: str = "", redirect_url: str = "") -> str: + def connect_select_facebook_page( + profile_id: str, + page_id: str, + temp_token: str, + user_profile: str = "", + redirect_url: str = "", + ) -> str: """Select Facebook page Args: @@ -1342,11 +1741,16 @@ def connect_select_facebook_page(profile_id: str, page_id: str, temp_token: str, redirect_url: Optional custom redirect URL to return to after selection""" client = _get_client() try: - response = client.connect.select_facebook_page(profile_id=profile_id, page_id=page_id, temp_token=temp_token, user_profile=user_profile, redirect_url=redirect_url) + response = client.connect.select_facebook_page( + profile_id=profile_id, + page_id=page_id, + temp_token=temp_token, + user_profile=user_profile, + redirect_url=redirect_url, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_list_google_business_locations(profile_id: str, temp_token: str) -> str: @@ -1357,14 +1761,21 @@ def connect_list_google_business_locations(profile_id: str, temp_token: str) -> temp_token: Temporary Google access token from the OAuth callback redirect (required)""" client = _get_client() try: - response = client.connect.list_google_business_locations(profile_id=profile_id, temp_token=temp_token) + response = client.connect.list_google_business_locations( + profile_id=profile_id, temp_token=temp_token + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_select_google_business_location(profile_id: str, location_id: str, temp_token: str, user_profile: str = "", redirect_url: str = "") -> str: + def connect_select_google_business_location( + profile_id: str, + location_id: str, + temp_token: str, + user_profile: str = "", + redirect_url: str = "", + ) -> str: """Select GBP location Args: @@ -1375,11 +1786,16 @@ def connect_select_google_business_location(profile_id: str, location_id: str, t redirect_url: Optional custom redirect URL to return to after selection""" client = _get_client() try: - response = client.connect.select_google_business_location(profile_id=profile_id, location_id=location_id, temp_token=temp_token, user_profile=user_profile, redirect_url=redirect_url) + response = client.connect.select_google_business_location( + profile_id=profile_id, + location_id=location_id, + temp_token=temp_token, + user_profile=user_profile, + redirect_url=redirect_url, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_pending_o_auth_data(token: str) -> str: @@ -1392,8 +1808,7 @@ def connect_get_pending_o_auth_data(token: str) -> str: response = client.connect.get_pending_o_auth_data(token=token) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_list_linked_in_organizations(temp_token: str, org_ids: str) -> str: @@ -1404,14 +1819,22 @@ def connect_list_linked_in_organizations(temp_token: str, org_ids: str) -> str: org_ids: Comma-separated list of organization IDs to fetch details for (max 100) (required)""" client = _get_client() try: - response = client.connect.list_linked_in_organizations(temp_token=temp_token, org_ids=org_ids) + response = client.connect.list_linked_in_organizations( + temp_token=temp_token, org_ids=org_ids + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_select_linked_in_organization(profile_id: str, temp_token: str, user_profile: str, account_type: str, selected_organization: str = "", redirect_url: str = "") -> str: + def connect_select_linked_in_organization( + profile_id: str, + temp_token: str, + user_profile: str, + account_type: str, + selected_organization: str = "", + redirect_url: str = "", + ) -> str: """Select LinkedIn org Args: @@ -1423,14 +1846,22 @@ def connect_select_linked_in_organization(profile_id: str, temp_token: str, user redirect_url""" client = _get_client() try: - response = client.connect.select_linked_in_organization(profile_id=profile_id, temp_token=temp_token, user_profile=user_profile, account_type=account_type, selected_organization=selected_organization, redirect_url=redirect_url) + response = client.connect.select_linked_in_organization( + profile_id=profile_id, + temp_token=temp_token, + user_profile=user_profile, + account_type=account_type, + selected_organization=selected_organization, + redirect_url=redirect_url, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_list_pinterest_boards_for_selection(profile_id: str, temp_token: str) -> str: + def connect_list_pinterest_boards_for_selection( + profile_id: str, temp_token: str + ) -> str: """List Pinterest boards Args: @@ -1438,14 +1869,24 @@ def connect_list_pinterest_boards_for_selection(profile_id: str, temp_token: str temp_token: Temporary Pinterest access token from the OAuth callback redirect (required)""" client = _get_client() try: - response = client.connect.list_pinterest_boards_for_selection(profile_id=profile_id, temp_token=temp_token) + response = client.connect.list_pinterest_boards_for_selection( + profile_id=profile_id, temp_token=temp_token + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_select_pinterest_board(profile_id: str, board_id: str, temp_token: str, board_name: str = "", user_profile: str = "", refresh_token: str = "", expires_in: int = 0, redirect_url: str = "") -> str: + def connect_select_pinterest_board( + profile_id: str, + board_id: str, + temp_token: str, + board_name: str = "", + user_profile: str = "", + refresh_token: str = "", + expires_in: int = 0, + redirect_url: str = "", + ) -> str: """Select Pinterest board Args: @@ -1459,11 +1900,19 @@ def connect_select_pinterest_board(profile_id: str, board_id: str, temp_token: s redirect_url: Custom redirect URL after connection completes""" client = _get_client() try: - response = client.connect.select_pinterest_board(profile_id=profile_id, board_id=board_id, board_name=board_name, temp_token=temp_token, user_profile=user_profile, refresh_token=refresh_token, expires_in=expires_in, redirect_url=redirect_url) + response = client.connect.select_pinterest_board( + profile_id=profile_id, + board_id=board_id, + board_name=board_name, + temp_token=temp_token, + user_profile=user_profile, + refresh_token=refresh_token, + expires_in=expires_in, + redirect_url=redirect_url, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_list_snapchat_profiles(profile_id: str, temp_token: str) -> str: @@ -1474,14 +1923,23 @@ def connect_list_snapchat_profiles(profile_id: str, temp_token: str) -> str: temp_token: Temporary Snapchat access token from the OAuth callback redirect (required)""" client = _get_client() try: - response = client.connect.list_snapchat_profiles(profile_id=profile_id, temp_token=temp_token) + response = client.connect.list_snapchat_profiles( + profile_id=profile_id, temp_token=temp_token + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_select_snapchat_profile(profile_id: str, selected_public_profile: str, temp_token: str, user_profile: str, refresh_token: str = "", expires_in: int = 0, redirect_url: str = "") -> str: + def connect_select_snapchat_profile( + profile_id: str, + selected_public_profile: str, + temp_token: str, + user_profile: str, + refresh_token: str = "", + expires_in: int = 0, + redirect_url: str = "", + ) -> str: """Select Snapchat profile Args: @@ -1494,14 +1952,23 @@ def connect_select_snapchat_profile(profile_id: str, selected_public_profile: st redirect_url: Custom redirect URL after connection completes""" client = _get_client() try: - response = client.connect.select_snapchat_profile(profile_id=profile_id, selected_public_profile=selected_public_profile, temp_token=temp_token, user_profile=user_profile, refresh_token=refresh_token, expires_in=expires_in, redirect_url=redirect_url) + response = client.connect.select_snapchat_profile( + profile_id=profile_id, + selected_public_profile=selected_public_profile, + temp_token=temp_token, + user_profile=user_profile, + refresh_token=refresh_token, + expires_in=expires_in, + redirect_url=redirect_url, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_bluesky_credentials(identifier: str, app_password: str, state: str, redirect_uri: str = "") -> str: + def connect_bluesky_credentials( + identifier: str, app_password: str, state: str, redirect_uri: str = "" + ) -> str: """Connect Bluesky account Args: @@ -1511,14 +1978,20 @@ def connect_bluesky_credentials(identifier: str, app_password: str, state: str, redirect_uri: Optional URL to redirect to after successful connection""" client = _get_client() try: - response = client.connect.connect_bluesky_credentials(identifier=identifier, app_password=app_password, state=state, redirect_uri=redirect_uri) + response = client.connect.connect_bluesky_credentials( + identifier=identifier, + app_password=app_password, + state=state, + redirect_uri=redirect_uri, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_whats_app_credentials(profile_id: str, access_token: str, waba_id: str, phone_number_id: str) -> str: + def connect_whats_app_credentials( + profile_id: str, access_token: str, waba_id: str, phone_number_id: str + ) -> str: """Connect WhatsApp via credentials Args: @@ -1528,11 +2001,15 @@ def connect_whats_app_credentials(profile_id: str, access_token: str, waba_id: s phone_number_id: Phone Number ID from Meta WhatsApp Manager (required)""" client = _get_client() try: - response = client.connect.connect_whats_app_credentials(profile_id=profile_id, access_token=access_token, waba_id=waba_id, phone_number_id=phone_number_id) + response = client.connect.connect_whats_app_credentials( + profile_id=profile_id, + access_token=access_token, + waba_id=waba_id, + phone_number_id=phone_number_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_telegram_connect_status(profile_id: str) -> str: @@ -1545,8 +2022,7 @@ def connect_get_telegram_connect_status(profile_id: str) -> str: response = client.connect.get_telegram_connect_status(profile_id=profile_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_initiate_telegram_connect(chat_id: str, profile_id: str) -> str: @@ -1557,11 +2033,12 @@ def connect_initiate_telegram_connect(chat_id: str, profile_id: str) -> str: profile_id: The profile ID to connect the account to (required)""" client = _get_client() try: - response = client.connect.initiate_telegram_connect(chat_id=chat_id, profile_id=profile_id) + response = client.connect.initiate_telegram_connect( + chat_id=chat_id, profile_id=profile_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_complete_telegram_connect(code: str) -> str: @@ -1574,8 +2051,7 @@ def connect_complete_telegram_connect(code: str) -> str: response = client.connect.complete_telegram_connect(code=code) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_facebook_pages(account_id: str) -> str: @@ -1588,8 +2064,7 @@ def connect_get_facebook_pages(account_id: str) -> str: response = client.connect.get_facebook_pages(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_update_facebook_page(account_id: str, selected_page_id: str) -> str: @@ -1600,11 +2075,12 @@ def connect_update_facebook_page(account_id: str, selected_page_id: str) -> str: selected_page_id: (required)""" client = _get_client() try: - response = client.connect.update_facebook_page(account_id=account_id, selected_page_id=selected_page_id) + response = client.connect.update_facebook_page( + account_id=account_id, selected_page_id=selected_page_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_linked_in_organizations(account_id: str) -> str: @@ -1617,11 +2093,12 @@ def connect_get_linked_in_organizations(account_id: str) -> str: response = client.connect.get_linked_in_organizations(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_update_linked_in_organization(account_id: str, account_type: str, selected_organization: str = "") -> str: + def connect_update_linked_in_organization( + account_id: str, account_type: str, selected_organization: str = "" + ) -> str: """Switch LinkedIn account type Args: @@ -1630,11 +2107,14 @@ def connect_update_linked_in_organization(account_id: str, account_type: str, se selected_organization""" client = _get_client() try: - response = client.connect.update_linked_in_organization(account_id=account_id, account_type=account_type, selected_organization=selected_organization) + response = client.connect.update_linked_in_organization( + account_id=account_id, + account_type=account_type, + selected_organization=selected_organization, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_pinterest_boards(account_id: str) -> str: @@ -1647,11 +2127,12 @@ def connect_get_pinterest_boards(account_id: str) -> str: response = client.connect.get_pinterest_boards(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_update_pinterest_boards(account_id: str, default_board_id: str, default_board_name: str = "") -> str: + def connect_update_pinterest_boards( + account_id: str, default_board_id: str, default_board_name: str = "" + ) -> str: """Set default Pinterest board Args: @@ -1660,11 +2141,14 @@ def connect_update_pinterest_boards(account_id: str, default_board_id: str, defa default_board_name""" client = _get_client() try: - response = client.connect.update_pinterest_boards(account_id=account_id, default_board_id=default_board_id, default_board_name=default_board_name) + response = client.connect.update_pinterest_boards( + account_id=account_id, + default_board_id=default_board_id, + default_board_name=default_board_name, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_gmb_locations(account_id: str) -> str: @@ -1677,8 +2161,7 @@ def connect_get_gmb_locations(account_id: str) -> str: response = client.connect.get_gmb_locations(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_update_gmb_location(account_id: str, selected_location_id: str) -> str: @@ -1689,11 +2172,12 @@ def connect_update_gmb_location(account_id: str, selected_location_id: str) -> s selected_location_id: (required)""" client = _get_client() try: - response = client.connect.update_gmb_location(account_id=account_id, selected_location_id=selected_location_id) + response = client.connect.update_gmb_location( + account_id=account_id, selected_location_id=selected_location_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_reddit_subreddits(account_id: str) -> str: @@ -1706,11 +2190,12 @@ def connect_get_reddit_subreddits(account_id: str) -> str: response = client.connect.get_reddit_subreddits(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def connect_update_reddit_subreddits(account_id: str, default_subreddit: str) -> str: + def connect_update_reddit_subreddits( + account_id: str, default_subreddit: str + ) -> str: """Set default subreddit Args: @@ -1718,11 +2203,12 @@ def connect_update_reddit_subreddits(account_id: str, default_subreddit: str) -> default_subreddit: (required)""" client = _get_client() try: - response = client.connect.update_reddit_subreddits(account_id=account_id, default_subreddit=default_subreddit) + response = client.connect.update_reddit_subreddits( + account_id=account_id, default_subreddit=default_subreddit + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def connect_get_reddit_flairs(account_id: str, subreddit: str) -> str: @@ -1733,16 +2219,25 @@ def connect_get_reddit_flairs(account_id: str, subreddit: str) -> str: subreddit: Subreddit name (without "r/" prefix) to fetch flairs for (required)""" client = _get_client() try: - response = client.connect.get_reddit_flairs(account_id=account_id, subreddit=subreddit) + response = client.connect.get_reddit_flairs( + account_id=account_id, subreddit=subreddit + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # CONTACTS - @mcp.tool() - def contacts_list_contacts(profile_id: str = "", search: str = "", tag: str = "", platform: str = "", is_subscribed: str = "", limit: int = 50, skip: int = 0) -> str: + def contacts_list_contacts( + profile_id: str = "", + search: str = "", + tag: str = "", + platform: str = "", + is_subscribed: str = "", + limit: int = 50, + skip: int = 0, + ) -> str: """List contacts Args: @@ -1755,14 +2250,33 @@ def contacts_list_contacts(profile_id: str = "", search: str = "", tag: str = "" skip""" client = _get_client() try: - response = client.contacts.list_contacts(profile_id=profile_id, search=search, tag=tag, platform=platform, is_subscribed=is_subscribed, limit=limit, skip=skip) - return _format_response(response) - except Exception as e: - return f'Error: {e}' - - - @mcp.tool() - def contacts_create_contact(profile_id: str, name: str, email: str = "", company: str = "", tags: str = "", is_subscribed: bool = True, notes: str = "", account_id: str = "", platform: str = "", platform_identifier: str = "", display_identifier: str = "") -> str: + response = client.contacts.list_contacts( + profile_id=profile_id, + search=search, + tag=tag, + platform=platform, + is_subscribed=is_subscribed, + limit=limit, + skip=skip, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def contacts_create_contact( + profile_id: str, + name: str, + email: str = "", + company: str = "", + tags: str = "", + is_subscribed: bool = True, + notes: str = "", + account_id: str = "", + platform: str = "", + platform_identifier: str = "", + display_identifier: str = "", + ) -> str: """Create a contact Args: @@ -1779,11 +2293,22 @@ def contacts_create_contact(profile_id: str, name: str, email: str = "", company display_identifier""" client = _get_client() try: - response = client.contacts.create_contact(profile_id=profile_id, name=name, email=email, company=company, tags=tags, is_subscribed=is_subscribed, notes=notes, account_id=account_id, platform=platform, platform_identifier=platform_identifier, display_identifier=display_identifier) + response = client.contacts.create_contact( + profile_id=profile_id, + name=name, + email=email, + company=company, + tags=tags, + is_subscribed=is_subscribed, + notes=notes, + account_id=account_id, + platform=platform, + platform_identifier=platform_identifier, + display_identifier=display_identifier, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def contacts_get_contact(contact_id: str) -> str: @@ -1796,11 +2321,20 @@ def contacts_get_contact(contact_id: str) -> str: response = client.contacts.get_contact(contact_id=contact_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def contacts_update_contact(contact_id: str, name: str = "", email: str = "", company: str = "", avatar_url: str = "", tags: str = "", is_subscribed: bool = False, is_blocked: bool = False, notes: str = "") -> str: + def contacts_update_contact( + contact_id: str, + name: str = "", + email: str = "", + company: str = "", + avatar_url: str = "", + tags: str = "", + is_subscribed: bool = False, + is_blocked: bool = False, + notes: str = "", + ) -> str: """Update a contact Args: @@ -1815,11 +2349,20 @@ def contacts_update_contact(contact_id: str, name: str = "", email: str = "", co notes""" client = _get_client() try: - response = client.contacts.update_contact(contact_id=contact_id, name=name, email=email, company=company, avatar_url=avatar_url, tags=tags, is_subscribed=is_subscribed, is_blocked=is_blocked, notes=notes) + response = client.contacts.update_contact( + contact_id=contact_id, + name=name, + email=email, + company=company, + avatar_url=avatar_url, + tags=tags, + is_subscribed=is_subscribed, + is_blocked=is_blocked, + notes=notes, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def contacts_delete_contact(contact_id: str) -> str: @@ -1832,8 +2375,7 @@ def contacts_delete_contact(contact_id: str) -> str: response = client.contacts.delete_contact(contact_id=contact_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def contacts_get_contact_channels(contact_id: str) -> str: @@ -1846,11 +2388,12 @@ def contacts_get_contact_channels(contact_id: str) -> str: response = client.contacts.get_contact_channels(contact_id=contact_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def contacts_bulk_create_contacts(profile_id: str, account_id: str, platform: str, contacts: str) -> str: + def contacts_bulk_create_contacts( + profile_id: str, account_id: str, platform: str, contacts: str + ) -> str: """Bulk create contacts Args: @@ -1860,16 +2403,22 @@ def contacts_bulk_create_contacts(profile_id: str, account_id: str, platform: st contacts: (required)""" client = _get_client() try: - response = client.contacts.bulk_create_contacts(profile_id=profile_id, account_id=account_id, platform=platform, contacts=contacts) + response = client.contacts.bulk_create_contacts( + profile_id=profile_id, + account_id=account_id, + platform=platform, + contacts=contacts, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # CUSTOM_FIELDS - @mcp.tool() - def custom_fields_set_contact_field_value(contact_id: str, slug: str, value: str) -> str: + def custom_fields_set_contact_field_value( + contact_id: str, slug: str, value: str + ) -> str: """Set a custom field value Args: @@ -1878,11 +2427,12 @@ def custom_fields_set_contact_field_value(contact_id: str, slug: str, value: str value: Field value (type depends on field definition) (required)""" client = _get_client() try: - response = client.custom_fields.set_contact_field_value(contact_id=contact_id, slug=slug, value=value) + response = client.custom_fields.set_contact_field_value( + contact_id=contact_id, slug=slug, value=value + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def custom_fields_clear_contact_field_value(contact_id: str, slug: str) -> str: @@ -1893,11 +2443,12 @@ def custom_fields_clear_contact_field_value(contact_id: str, slug: str) -> str: slug: (required)""" client = _get_client() try: - response = client.custom_fields.clear_contact_field_value(contact_id=contact_id, slug=slug) + response = client.custom_fields.clear_contact_field_value( + contact_id=contact_id, slug=slug + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def custom_fields_list_custom_fields(profile_id: str = "") -> str: @@ -1910,11 +2461,12 @@ def custom_fields_list_custom_fields(profile_id: str = "") -> str: response = client.custom_fields.list_custom_fields(profile_id=profile_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def custom_fields_create_custom_field(profile_id: str, name: str, type: str, slug: str = "", options: str = "") -> str: + def custom_fields_create_custom_field( + profile_id: str, name: str, type: str, slug: str = "", options: str = "" + ) -> str: """Create a custom field definition Args: @@ -1925,14 +2477,17 @@ def custom_fields_create_custom_field(profile_id: str, name: str, type: str, slu options: Required for select type""" client = _get_client() try: - response = client.custom_fields.create_custom_field(profile_id=profile_id, name=name, slug=slug, type=type, options=options) + response = client.custom_fields.create_custom_field( + profile_id=profile_id, name=name, slug=slug, type=type, options=options + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def custom_fields_update_custom_field(field_id: str, name: str = "", options: str = "") -> str: + def custom_fields_update_custom_field( + field_id: str, name: str = "", options: str = "" + ) -> str: """Update a custom field definition Args: @@ -1941,11 +2496,12 @@ def custom_fields_update_custom_field(field_id: str, name: str = "", options: st options""" client = _get_client() try: - response = client.custom_fields.update_custom_field(field_id=field_id, name=name, options=options) + response = client.custom_fields.update_custom_field( + field_id=field_id, name=name, options=options + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def custom_fields_delete_custom_field(field_id: str) -> str: @@ -1958,11 +2514,10 @@ def custom_fields_delete_custom_field(field_id: str) -> str: response = client.custom_fields.delete_custom_field(field_id=field_id) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # INVITES - @mcp.tool() def invites_create_invite_token(scope: str, profile_ids: str = "") -> str: """Create invite token @@ -1972,16 +2527,25 @@ def invites_create_invite_token(scope: str, profile_ids: str = "") -> str: profile_ids: Required if scope is 'profiles'. Array of profile IDs to grant access to.""" client = _get_client() try: - response = client.invites.create_invite_token(scope=scope, profile_ids=profile_ids) + response = client.invites.create_invite_token( + scope=scope, profile_ids=profile_ids + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # LOGS - @mcp.tool() - def logs_list_posts_logs(status: str = "", platform: str = "", action: str = "", days: int = 7, limit: int = 50, skip: int = 0, search: str = "") -> str: + def logs_list_posts_logs( + status: str = "", + platform: str = "", + action: str = "", + days: int = 7, + limit: int = 50, + skip: int = 0, + search: str = "", + ) -> str: """List publishing logs Args: @@ -1994,14 +2558,28 @@ def logs_list_posts_logs(status: str = "", platform: str = "", action: str = "", search: Search through log entries by text content.""" client = _get_client() try: - response = client.logs.list_posts_logs(status=status, platform=platform, action=action, days=days, limit=limit, skip=skip, search=search) + response = client.logs.list_posts_logs( + status=status, + platform=platform, + action=action, + days=days, + limit=limit, + skip=skip, + search=search, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def logs_list_connection_logs(platform: str = "", event_type: str = "", status: str = "", days: int = 7, limit: int = 50, skip: int = 0) -> str: + def logs_list_connection_logs( + platform: str = "", + event_type: str = "", + status: str = "", + days: int = 7, + limit: int = 50, + skip: int = 0, + ) -> str: """List connection logs Args: @@ -2013,11 +2591,17 @@ def logs_list_connection_logs(platform: str = "", event_type: str = "", status: skip: Number of logs to skip (for pagination)""" client = _get_client() try: - response = client.logs.list_connection_logs(platform=platform, event_type=event_type, status=status, days=days, limit=limit, skip=skip) + response = client.logs.list_connection_logs( + platform=platform, + event_type=event_type, + status=status, + days=days, + limit=limit, + skip=skip, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def logs_get_post_logs(post_id: str, limit: int = 50) -> str: @@ -2031,13 +2615,14 @@ def logs_get_post_logs(post_id: str, limit: int = 50) -> str: response = client.logs.get_post_logs(post_id=post_id, limit=limit) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # MEDIA - @mcp.tool() - def media_get_media_presigned_url(filename: str, content_type: str, size: int = 0) -> str: + def media_get_media_presigned_url( + filename: str, content_type: str, size: int = 0 + ) -> str: """Get presigned upload URL Args: @@ -2046,16 +2631,25 @@ def media_get_media_presigned_url(filename: str, content_type: str, size: int = size: Optional file size in bytes for pre-validation (max 5GB)""" client = _get_client() try: - response = client.media.get_media_presigned_url(filename=filename, content_type=content_type, size=size) + response = client.media.get_media_presigned_url( + filename=filename, content_type=content_type, size=size + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # MESSAGES - @mcp.tool() - def messages_list_inbox_conversations(profile_id: str = "", platform: str = "", status: str = "", sort_order: str = "desc", limit: int = 50, cursor: str = "", account_id: str = "") -> str: + def messages_list_inbox_conversations( + profile_id: str = "", + platform: str = "", + status: str = "", + sort_order: str = "desc", + limit: int = 50, + cursor: str = "", + account_id: str = "", + ) -> str: """List conversations Args: @@ -2068,11 +2662,18 @@ def messages_list_inbox_conversations(profile_id: str = "", platform: str = "", account_id: Filter by specific social account ID""" client = _get_client() try: - response = client.messages.list_inbox_conversations(profile_id=profile_id, platform=platform, status=status, sort_order=sort_order, limit=limit, cursor=cursor, account_id=account_id) + response = client.messages.list_inbox_conversations( + profile_id=profile_id, + platform=platform, + status=status, + sort_order=sort_order, + limit=limit, + cursor=cursor, + account_id=account_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def messages_get_inbox_conversation(conversation_id: str, account_id: str) -> str: @@ -2083,14 +2684,17 @@ def messages_get_inbox_conversation(conversation_id: str, account_id: str) -> st account_id: The social account ID (required)""" client = _get_client() try: - response = client.messages.get_inbox_conversation(conversation_id=conversation_id, account_id=account_id) + response = client.messages.get_inbox_conversation( + conversation_id=conversation_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def messages_update_inbox_conversation(conversation_id: str, account_id: str, status: str) -> str: + def messages_update_inbox_conversation( + conversation_id: str, account_id: str, status: str + ) -> str: """Update conversation status Args: @@ -2099,14 +2703,17 @@ def messages_update_inbox_conversation(conversation_id: str, account_id: str, st status: (required)""" client = _get_client() try: - response = client.messages.update_inbox_conversation(conversation_id=conversation_id, account_id=account_id, status=status) + response = client.messages.update_inbox_conversation( + conversation_id=conversation_id, account_id=account_id, status=status + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def messages_get_inbox_conversation_messages(conversation_id: str, account_id: str) -> str: + def messages_get_inbox_conversation_messages( + conversation_id: str, account_id: str + ) -> str: """List messages Args: @@ -2114,14 +2721,28 @@ def messages_get_inbox_conversation_messages(conversation_id: str, account_id: s account_id: Social account ID (required)""" client = _get_client() try: - response = client.messages.get_inbox_conversation_messages(conversation_id=conversation_id, account_id=account_id) + response = client.messages.get_inbox_conversation_messages( + conversation_id=conversation_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def messages_send_inbox_message(conversation_id: str, account_id: str, message: str = "", attachment_url: str = "", attachment_type: str = "", quick_replies: str = "", buttons: str = "", template: str = "", reply_markup: str = "", messaging_type: str = "", message_tag: str = "", reply_to: str = "") -> str: + def messages_send_inbox_message( + conversation_id: str, + account_id: str, + message: str = "", + attachment_url: str = "", + attachment_type: str = "", + quick_replies: str = "", + buttons: str = "", + template: str = "", + reply_markup: str = "", + messaging_type: str = "", + message_tag: str = "", + reply_to: str = "", + ) -> str: """Send message Args: @@ -2139,14 +2760,32 @@ def messages_send_inbox_message(conversation_id: str, account_id: str, message: reply_to: Platform message ID to reply to (Telegram only).""" client = _get_client() try: - response = client.messages.send_inbox_message(conversation_id=conversation_id, account_id=account_id, message=message, attachment_url=attachment_url, attachment_type=attachment_type, quick_replies=quick_replies, buttons=buttons, template=template, reply_markup=reply_markup, messaging_type=messaging_type, message_tag=message_tag, reply_to=reply_to) - return _format_response(response) - except Exception as e: - return f'Error: {e}' - - - @mcp.tool() - def messages_edit_inbox_message(conversation_id: str, message_id: str, account_id: str, text: str = "", reply_markup: str = "") -> str: + response = client.messages.send_inbox_message( + conversation_id=conversation_id, + account_id=account_id, + message=message, + attachment_url=attachment_url, + attachment_type=attachment_type, + quick_replies=quick_replies, + buttons=buttons, + template=template, + reply_markup=reply_markup, + messaging_type=messaging_type, + message_tag=message_tag, + reply_to=reply_to, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def messages_edit_inbox_message( + conversation_id: str, + message_id: str, + account_id: str, + text: str = "", + reply_markup: str = "", + ) -> str: """Edit message Args: @@ -2157,16 +2796,33 @@ def messages_edit_inbox_message(conversation_id: str, message_id: str, account_i reply_markup: New inline keyboard markup""" client = _get_client() try: - response = client.messages.edit_inbox_message(conversation_id=conversation_id, message_id=message_id, account_id=account_id, text=text, reply_markup=reply_markup) + response = client.messages.edit_inbox_message( + conversation_id=conversation_id, + message_id=message_id, + account_id=account_id, + text=text, + reply_markup=reply_markup, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # POSTS - @mcp.tool() - def posts_list_posts(page: int = 1, limit: int = 10, status: str = "", platform: str = "", profile_id: str = "", created_by: str = "", date_from: str = "", date_to: str = "", include_hidden: bool = False, search: str = "", sort_by: str = "scheduled-desc") -> str: + def posts_list_posts( + page: int = 1, + limit: int = 10, + status: str = "", + platform: str = "", + profile_id: str = "", + created_by: str = "", + date_from: str = "", + date_to: str = "", + include_hidden: bool = False, + search: str = "", + sort_by: str = "scheduled-desc", + ) -> str: """List posts Args: @@ -2183,11 +2839,22 @@ def posts_list_posts(page: int = 1, limit: int = 10, status: str = "", platform: sort_by: Sort order for results.""" client = _get_client() try: - response = client.posts.list_posts(page=page, limit=limit, status=status, platform=platform, profile_id=profile_id, created_by=created_by, date_from=date_from, date_to=date_to, include_hidden=include_hidden, search=search, sort_by=sort_by) + response = client.posts.list_posts( + page=page, + limit=limit, + status=status, + platform=platform, + profile_id=profile_id, + created_by=created_by, + date_from=date_from, + date_to=date_to, + include_hidden=include_hidden, + search=search, + sort_by=sort_by, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def posts_get_post(post_id: str) -> str: @@ -2200,11 +2867,16 @@ def posts_get_post(post_id: str) -> str: response = client.posts.get_post(post_id=post_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def posts_update_post(post_id: str, content: str = "", scheduled_for: str = "", tiktok_settings: str = "", recycling: str = "") -> str: + def posts_update_post( + post_id: str, + content: str = "", + scheduled_for: str = "", + tiktok_settings: str = "", + recycling: str = "", + ) -> str: """Update post Args: @@ -2215,11 +2887,16 @@ def posts_update_post(post_id: str, content: str = "", scheduled_for: str = "", recycling""" client = _get_client() try: - response = client.posts.update_post(post_id=post_id, content=content, scheduled_for=scheduled_for, tiktok_settings=tiktok_settings, recycling=recycling) + response = client.posts.update_post( + post_id=post_id, + content=content, + scheduled_for=scheduled_for, + tiktok_settings=tiktok_settings, + recycling=recycling, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def posts_delete_post(post_id: str) -> str: @@ -2232,8 +2909,7 @@ def posts_delete_post(post_id: str) -> str: response = client.posts.delete_post(post_id=post_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def posts_bulk_upload_posts(dry_run: bool = False) -> str: @@ -2246,8 +2922,7 @@ def posts_bulk_upload_posts(dry_run: bool = False) -> str: response = client.posts.bulk_upload_posts(dry_run=dry_run) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def posts_unpublish_post(post_id: str, platform: str) -> str: @@ -2261,16 +2936,27 @@ def posts_unpublish_post(post_id: str, platform: str) -> str: response = client.posts.unpublish_post(post_id=post_id, platform=platform) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def posts_update_post_metadata(post_id: str, platform: str, title: str = "", description: str = "", tags: str = "", category_id: str = "", privacy_status: str = "") -> str: + def posts_update_post_metadata( + post_id: str, + platform: str, + video_id: str = "", + account_id: str = "", + title: str = "", + description: str = "", + tags: str = "", + category_id: str = "", + privacy_status: str = "", + ) -> str: """Update post metadata Args: - post_id: (required) + post_id: Zernio post ID, or "_" when using direct video ID mode (required) platform: The platform to update metadata on (required) + video_id: YouTube video ID (required for direct mode, ignored for post-based mode) + account_id: Zernio social account ID (required for direct mode, ignored for post-based mode) title: New video title (max 100 characters for YouTube) description: New video description tags: Array of keyword tags (max 500 characters combined for YouTube) @@ -2278,14 +2964,23 @@ def posts_update_post_metadata(post_id: str, platform: str, title: str = "", des privacy_status: Video privacy setting""" client = _get_client() try: - response = client.posts.update_post_metadata(post_id=post_id, platform=platform, title=title, description=description, tags=tags, category_id=category_id, privacy_status=privacy_status) + response = client.posts.update_post_metadata( + post_id=post_id, + platform=platform, + video_id=video_id, + account_id=account_id, + title=title, + description=description, + tags=tags, + category_id=category_id, + privacy_status=privacy_status, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # PROFILES - @mcp.tool() def profiles_list_profiles(include_over_limit: bool = False) -> str: """List profiles @@ -2294,14 +2989,17 @@ def profiles_list_profiles(include_over_limit: bool = False) -> str: include_over_limit: When true, includes over-limit profiles (marked with isOverLimit: true).""" client = _get_client() try: - response = client.profiles.list_profiles(include_over_limit=include_over_limit) + response = client.profiles.list_profiles( + include_over_limit=include_over_limit + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def profiles_create_profile(name: str, description: str = "", color: str = "") -> str: + def profiles_create_profile( + name: str, description: str = "", color: str = "" + ) -> str: """Create profile Args: @@ -2310,11 +3008,12 @@ def profiles_create_profile(name: str, description: str = "", color: str = "") - color""" client = _get_client() try: - response = client.profiles.create_profile(name=name, description=description, color=color) + response = client.profiles.create_profile( + name=name, description=description, color=color + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def profiles_get_profile(profile_id: str) -> str: @@ -2327,11 +3026,16 @@ def profiles_get_profile(profile_id: str) -> str: response = client.profiles.get_profile(profile_id=profile_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def profiles_update_profile(profile_id: str, name: str = "", description: str = "", color: str = "", is_default: bool = False) -> str: + def profiles_update_profile( + profile_id: str, + name: str = "", + description: str = "", + color: str = "", + is_default: bool = False, + ) -> str: """Update profile Args: @@ -2342,11 +3046,16 @@ def profiles_update_profile(profile_id: str, name: str = "", description: str = is_default""" client = _get_client() try: - response = client.profiles.update_profile(profile_id=profile_id, name=name, description=description, color=color, is_default=is_default) + response = client.profiles.update_profile( + profile_id=profile_id, + name=name, + description=description, + color=color, + is_default=is_default, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def profiles_delete_profile(profile_id: str) -> str: @@ -2359,13 +3068,14 @@ def profiles_delete_profile(profile_id: str) -> str: response = client.profiles.delete_profile(profile_id=profile_id) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # QUEUE - @mcp.tool() - def queue_list_queue_slots(profile_id: str, queue_id: str = "", all: str = "") -> str: + def queue_list_queue_slots( + profile_id: str, queue_id: str = "", all: str = "" + ) -> str: """List schedules Args: @@ -2374,14 +3084,17 @@ def queue_list_queue_slots(profile_id: str, queue_id: str = "", all: str = "") - all: Set to 'true' to list all queues for the profile""" client = _get_client() try: - response = client.queue.list_queue_slots(profile_id=profile_id, queue_id=queue_id, all=all) + response = client.queue.list_queue_slots( + profile_id=profile_id, queue_id=queue_id, all=all + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def queue_create_queue_slot(profile_id: str, name: str, timezone: str, slots: str, active: bool = True) -> str: + def queue_create_queue_slot( + profile_id: str, name: str, timezone: str, slots: str, active: bool = True + ) -> str: """Create schedule Args: @@ -2392,14 +3105,28 @@ def queue_create_queue_slot(profile_id: str, name: str, timezone: str, slots: st active""" client = _get_client() try: - response = client.queue.create_queue_slot(profile_id=profile_id, name=name, timezone=timezone, slots=slots, active=active) + response = client.queue.create_queue_slot( + profile_id=profile_id, + name=name, + timezone=timezone, + slots=slots, + active=active, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def queue_update_queue_slot(profile_id: str, timezone: str, slots: str, queue_id: str = "", name: str = "", active: bool = True, set_as_default: bool = False, reshuffle_existing: bool = False) -> str: + def queue_update_queue_slot( + profile_id: str, + timezone: str, + slots: str, + queue_id: str = "", + name: str = "", + active: bool = True, + set_as_default: bool = False, + reshuffle_existing: bool = False, + ) -> str: """Update schedule Args: @@ -2413,11 +3140,19 @@ def queue_update_queue_slot(profile_id: str, timezone: str, slots: str, queue_id reshuffle_existing: Whether to reschedule existing queued posts to match new slots""" client = _get_client() try: - response = client.queue.update_queue_slot(profile_id=profile_id, queue_id=queue_id, name=name, timezone=timezone, slots=slots, active=active, set_as_default=set_as_default, reshuffle_existing=reshuffle_existing) + response = client.queue.update_queue_slot( + profile_id=profile_id, + queue_id=queue_id, + name=name, + timezone=timezone, + slots=slots, + active=active, + set_as_default=set_as_default, + reshuffle_existing=reshuffle_existing, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def queue_delete_queue_slot(profile_id: str, queue_id: str) -> str: @@ -2428,14 +3163,17 @@ def queue_delete_queue_slot(profile_id: str, queue_id: str) -> str: queue_id: Queue ID to delete (required)""" client = _get_client() try: - response = client.queue.delete_queue_slot(profile_id=profile_id, queue_id=queue_id) + response = client.queue.delete_queue_slot( + profile_id=profile_id, queue_id=queue_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def queue_preview_queue(profile_id: str, queue_id: str = "", count: int = 20) -> str: + def queue_preview_queue( + profile_id: str, queue_id: str = "", count: int = 20 + ) -> str: """Preview upcoming slots Args: @@ -2444,11 +3182,12 @@ def queue_preview_queue(profile_id: str, queue_id: str = "", count: int = 20) -> count""" client = _get_client() try: - response = client.queue.preview_queue(profile_id=profile_id, queue_id=queue_id, count=count) + response = client.queue.preview_queue( + profile_id=profile_id, queue_id=queue_id, count=count + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def queue_get_next_queue_slot(profile_id: str, queue_id: str = "") -> str: @@ -2459,16 +3198,25 @@ def queue_get_next_queue_slot(profile_id: str, queue_id: str = "") -> str: queue_id: Specific queue ID (optional, defaults to profile's default queue)""" client = _get_client() try: - response = client.queue.get_next_queue_slot(profile_id=profile_id, queue_id=queue_id) + response = client.queue.get_next_queue_slot( + profile_id=profile_id, queue_id=queue_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # REDDIT - @mcp.tool() - def reddit_search_reddit(account_id: str, q: str, subreddit: str = "", restrict_sr: str = "", sort: str = "new", limit: int = 25, after: str = "") -> str: + def reddit_search_reddit( + account_id: str, + q: str, + subreddit: str = "", + restrict_sr: str = "", + sort: str = "new", + limit: int = 25, + after: str = "", + ) -> str: """Search posts Args: @@ -2481,14 +3229,28 @@ def reddit_search_reddit(account_id: str, q: str, subreddit: str = "", restrict_ after""" client = _get_client() try: - response = client.reddit.search_reddit(account_id=account_id, subreddit=subreddit, q=q, restrict_sr=restrict_sr, sort=sort, limit=limit, after=after) + response = client.reddit.search_reddit( + account_id=account_id, + subreddit=subreddit, + q=q, + restrict_sr=restrict_sr, + sort=sort, + limit=limit, + after=after, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def reddit_get_reddit_feed(account_id: str, subreddit: str = "", sort: str = "hot", limit: int = 25, after: str = "", t: str = "") -> str: + def reddit_get_reddit_feed( + account_id: str, + subreddit: str = "", + sort: str = "hot", + limit: int = 25, + after: str = "", + t: str = "", + ) -> str: """Get subreddit feed Args: @@ -2500,16 +3262,33 @@ def reddit_get_reddit_feed(account_id: str, subreddit: str = "", sort: str = "ho t""" client = _get_client() try: - response = client.reddit.get_reddit_feed(account_id=account_id, subreddit=subreddit, sort=sort, limit=limit, after=after, t=t) + response = client.reddit.get_reddit_feed( + account_id=account_id, + subreddit=subreddit, + sort=sort, + limit=limit, + after=after, + t=t, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # REVIEWS - @mcp.tool() - def reviews_list_inbox_reviews(profile_id: str = "", platform: str = "", min_rating: int = 0, max_rating: int = 0, has_reply: bool = False, sort_by: str = "date", sort_order: str = "desc", limit: int = 25, cursor: str = "", account_id: str = "") -> str: + def reviews_list_inbox_reviews( + profile_id: str = "", + platform: str = "", + min_rating: int = 0, + max_rating: int = 0, + has_reply: bool = False, + sort_by: str = "date", + sort_order: str = "desc", + limit: int = 25, + cursor: str = "", + account_id: str = "", + ) -> str: """List reviews Args: @@ -2525,14 +3304,26 @@ def reviews_list_inbox_reviews(profile_id: str = "", platform: str = "", min_rat account_id: Filter by specific social account ID""" client = _get_client() try: - response = client.reviews.list_inbox_reviews(profile_id=profile_id, platform=platform, min_rating=min_rating, max_rating=max_rating, has_reply=has_reply, sort_by=sort_by, sort_order=sort_order, limit=limit, cursor=cursor, account_id=account_id) + response = client.reviews.list_inbox_reviews( + profile_id=profile_id, + platform=platform, + min_rating=min_rating, + max_rating=max_rating, + has_reply=has_reply, + sort_by=sort_by, + sort_order=sort_order, + limit=limit, + cursor=cursor, + account_id=account_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def reviews_reply_to_inbox_review(review_id: str, account_id: str, message: str) -> str: + def reviews_reply_to_inbox_review( + review_id: str, account_id: str, message: str + ) -> str: """Reply to review Args: @@ -2541,11 +3332,12 @@ def reviews_reply_to_inbox_review(review_id: str, account_id: str, message: str) message: (required)""" client = _get_client() try: - response = client.reviews.reply_to_inbox_review(review_id=review_id, account_id=account_id, message=message) + response = client.reviews.reply_to_inbox_review( + review_id=review_id, account_id=account_id, message=message + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def reviews_delete_inbox_review_reply(review_id: str, account_id: str) -> str: @@ -2556,16 +3348,19 @@ def reviews_delete_inbox_review_reply(review_id: str, account_id: str) -> str: account_id: (required)""" client = _get_client() try: - response = client.reviews.delete_inbox_review_reply(review_id=review_id, account_id=account_id) + response = client.reviews.delete_inbox_review_reply( + review_id=review_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # SEQUENCES - @mcp.tool() - def sequences_list_sequences(profile_id: str = "", status: str = "", limit: int = 50, skip: int = 0) -> str: + def sequences_list_sequences( + profile_id: str = "", status: str = "", limit: int = 50, skip: int = 0 + ) -> str: """List sequences Args: @@ -2575,14 +3370,24 @@ def sequences_list_sequences(profile_id: str = "", status: str = "", limit: int skip""" client = _get_client() try: - response = client.sequences.list_sequences(profile_id=profile_id, status=status, limit=limit, skip=skip) + response = client.sequences.list_sequences( + profile_id=profile_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def sequences_create_sequence(profile_id: str, account_id: str, platform: str, name: str, description: str = "", steps: str = "", exit_on_reply: bool = True, exit_on_unsubscribe: bool = True) -> str: + def sequences_create_sequence( + profile_id: str, + account_id: str, + platform: str, + name: str, + description: str = "", + steps: str = "", + exit_on_reply: bool = True, + exit_on_unsubscribe: bool = True, + ) -> str: """Create a sequence Args: @@ -2596,11 +3401,19 @@ def sequences_create_sequence(profile_id: str, account_id: str, platform: str, n exit_on_unsubscribe""" client = _get_client() try: - response = client.sequences.create_sequence(profile_id=profile_id, account_id=account_id, platform=platform, name=name, description=description, steps=steps, exit_on_reply=exit_on_reply, exit_on_unsubscribe=exit_on_unsubscribe) + response = client.sequences.create_sequence( + profile_id=profile_id, + account_id=account_id, + platform=platform, + name=name, + description=description, + steps=steps, + exit_on_reply=exit_on_reply, + exit_on_unsubscribe=exit_on_unsubscribe, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_get_sequence(sequence_id: str) -> str: @@ -2613,8 +3426,7 @@ def sequences_get_sequence(sequence_id: str) -> str: response = client.sequences.get_sequence(sequence_id=sequence_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_update_sequence(sequence_id: str) -> str: @@ -2627,8 +3439,7 @@ def sequences_update_sequence(sequence_id: str) -> str: response = client.sequences.update_sequence(sequence_id=sequence_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_delete_sequence(sequence_id: str) -> str: @@ -2641,8 +3452,7 @@ def sequences_delete_sequence(sequence_id: str) -> str: response = client.sequences.delete_sequence(sequence_id=sequence_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_activate_sequence(sequence_id: str) -> str: @@ -2655,8 +3465,7 @@ def sequences_activate_sequence(sequence_id: str) -> str: response = client.sequences.activate_sequence(sequence_id=sequence_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_pause_sequence(sequence_id: str) -> str: @@ -2669,11 +3478,12 @@ def sequences_pause_sequence(sequence_id: str) -> str: response = client.sequences.pause_sequence(sequence_id=sequence_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def sequences_enroll_contacts(sequence_id: str, contact_ids: str, channel_ids: str = "") -> str: + def sequences_enroll_contacts( + sequence_id: str, contact_ids: str, channel_ids: str = "" + ) -> str: """Enroll contacts in a sequence Args: @@ -2682,11 +3492,14 @@ def sequences_enroll_contacts(sequence_id: str, contact_ids: str, channel_ids: s channel_ids: Optional. Auto-detected if not provided.""" client = _get_client() try: - response = client.sequences.enroll_contacts(sequence_id=sequence_id, contact_ids=contact_ids, channel_ids=channel_ids) + response = client.sequences.enroll_contacts( + sequence_id=sequence_id, + contact_ids=contact_ids, + channel_ids=channel_ids, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def sequences_unenroll_contact(sequence_id: str, contact_id: str) -> str: @@ -2697,14 +3510,17 @@ def sequences_unenroll_contact(sequence_id: str, contact_id: str) -> str: contact_id: (required)""" client = _get_client() try: - response = client.sequences.unenroll_contact(sequence_id=sequence_id, contact_id=contact_id) + response = client.sequences.unenroll_contact( + sequence_id=sequence_id, contact_id=contact_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def sequences_list_sequence_enrollments(sequence_id: str, status: str = "", limit: int = 50, skip: int = 0) -> str: + def sequences_list_sequence_enrollments( + sequence_id: str, status: str = "", limit: int = 50, skip: int = 0 + ) -> str: """List enrollments for a sequence Args: @@ -2714,16 +3530,23 @@ def sequences_list_sequence_enrollments(sequence_id: str, status: str = "", limi skip""" client = _get_client() try: - response = client.sequences.list_sequence_enrollments(sequence_id=sequence_id, status=status, limit=limit, skip=skip) + response = client.sequences.list_sequence_enrollments( + sequence_id=sequence_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # TOOLS - @mcp.tool() - def tools_download_you_tube_video(url: str, action: str = "download", format: str = "video", quality: str = "hd", format_id: str = "") -> str: + def tools_download_you_tube_video( + url: str, + action: str = "download", + format: str = "video", + quality: str = "hd", + format_id: str = "", + ) -> str: """Download YouTube video Args: @@ -2734,11 +3557,16 @@ def tools_download_you_tube_video(url: str, action: str = "download", format: st format_id: Specific format ID from formats list""" client = _get_client() try: - response = client.tools.download_you_tube_video(url=url, action=action, format=format, quality=quality, format_id=format_id) + response = client.tools.download_you_tube_video( + url=url, + action=action, + format=format, + quality=quality, + format_id=format_id, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_get_you_tube_transcript(url: str, lang: str = "en") -> str: @@ -2752,8 +3580,7 @@ def tools_get_you_tube_transcript(url: str, lang: str = "en") -> str: response = client.tools.get_you_tube_transcript(url=url, lang=lang) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_download_instagram_media(url: str) -> str: @@ -2766,8 +3593,7 @@ def tools_download_instagram_media(url: str) -> str: response = client.tools.download_instagram_media(url=url) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_check_instagram_hashtags(hashtags: str) -> str: @@ -2780,11 +3606,12 @@ def tools_check_instagram_hashtags(hashtags: str) -> str: response = client.tools.check_instagram_hashtags(hashtags=hashtags) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def tools_download_tik_tok_video(url: str, action: str = "download", format_id: str = "") -> str: + def tools_download_tik_tok_video( + url: str, action: str = "download", format_id: str = "" + ) -> str: """Download TikTok video Args: @@ -2793,14 +3620,17 @@ def tools_download_tik_tok_video(url: str, action: str = "download", format_id: format_id: Specific format ID (0 = no watermark, etc.)""" client = _get_client() try: - response = client.tools.download_tik_tok_video(url=url, action=action, format_id=format_id) + response = client.tools.download_tik_tok_video( + url=url, action=action, format_id=format_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def tools_download_twitter_media(url: str, action: str = "download", format_id: str = "") -> str: + def tools_download_twitter_media( + url: str, action: str = "download", format_id: str = "" + ) -> str: """Download Twitter/X media Args: @@ -2809,11 +3639,12 @@ def tools_download_twitter_media(url: str, action: str = "download", format_id: format_id""" client = _get_client() try: - response = client.tools.download_twitter_media(url=url, action=action, format_id=format_id) + response = client.tools.download_twitter_media( + url=url, action=action, format_id=format_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_download_facebook_video(url: str) -> str: @@ -2826,8 +3657,7 @@ def tools_download_facebook_video(url: str) -> str: response = client.tools.download_facebook_video(url=url) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_download_linked_in_video(url: str) -> str: @@ -2840,8 +3670,7 @@ def tools_download_linked_in_video(url: str) -> str: response = client.tools.download_linked_in_video(url=url) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def tools_download_bluesky_media(url: str) -> str: @@ -2854,11 +3683,10 @@ def tools_download_bluesky_media(url: str) -> str: response = client.tools.download_bluesky_media(url=url) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # TWITTER_ENGAGEMENT - @mcp.tool() def twitter_engagement_retweet_post(account_id: str, tweet_id: str) -> str: """Retweet a post @@ -2868,11 +3696,12 @@ def twitter_engagement_retweet_post(account_id: str, tweet_id: str) -> str: tweet_id: The ID of the tweet to retweet (required)""" client = _get_client() try: - response = client.twitter_engagement.retweet_post(account_id=account_id, tweet_id=tweet_id) + response = client.twitter_engagement.retweet_post( + account_id=account_id, tweet_id=tweet_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def twitter_engagement_undo_retweet(account_id: str, tweet_id: str) -> str: @@ -2883,11 +3712,12 @@ def twitter_engagement_undo_retweet(account_id: str, tweet_id: str) -> str: tweet_id: The ID of the original tweet to un-retweet (required)""" client = _get_client() try: - response = client.twitter_engagement.undo_retweet(account_id=account_id, tweet_id=tweet_id) + response = client.twitter_engagement.undo_retweet( + account_id=account_id, tweet_id=tweet_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def twitter_engagement_bookmark_post(account_id: str, tweet_id: str) -> str: @@ -2898,11 +3728,12 @@ def twitter_engagement_bookmark_post(account_id: str, tweet_id: str) -> str: tweet_id: The ID of the tweet to bookmark (required)""" client = _get_client() try: - response = client.twitter_engagement.bookmark_post(account_id=account_id, tweet_id=tweet_id) + response = client.twitter_engagement.bookmark_post( + account_id=account_id, tweet_id=tweet_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def twitter_engagement_remove_bookmark(account_id: str, tweet_id: str) -> str: @@ -2913,11 +3744,12 @@ def twitter_engagement_remove_bookmark(account_id: str, tweet_id: str) -> str: tweet_id: The ID of the tweet to unbookmark (required)""" client = _get_client() try: - response = client.twitter_engagement.remove_bookmark(account_id=account_id, tweet_id=tweet_id) + response = client.twitter_engagement.remove_bookmark( + account_id=account_id, tweet_id=tweet_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def twitter_engagement_follow_user(account_id: str, target_user_id: str) -> str: @@ -2928,11 +3760,12 @@ def twitter_engagement_follow_user(account_id: str, target_user_id: str) -> str: target_user_id: The Twitter ID of the user to follow (required)""" client = _get_client() try: - response = client.twitter_engagement.follow_user(account_id=account_id, target_user_id=target_user_id) + response = client.twitter_engagement.follow_user( + account_id=account_id, target_user_id=target_user_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def twitter_engagement_unfollow_user(account_id: str, target_user_id: str) -> str: @@ -2943,14 +3776,15 @@ def twitter_engagement_unfollow_user(account_id: str, target_user_id: str) -> st target_user_id: The Twitter ID of the user to unfollow (required)""" client = _get_client() try: - response = client.twitter_engagement.unfollow_user(account_id=account_id, target_user_id=target_user_id) + response = client.twitter_engagement.unfollow_user( + account_id=account_id, target_user_id=target_user_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # USAGE - @mcp.tool() def usage_get_usage_stats() -> str: """Get plan and usage stats""" @@ -2959,11 +3793,10 @@ def usage_get_usage_stats() -> str: response = client.usage.get_usage_stats() return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # USERS - @mcp.tool() def users_list_users() -> str: """List users""" @@ -2972,8 +3805,7 @@ def users_list_users() -> str: response = client.users.list_users() return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def users_get_user(user_id: str) -> str: @@ -2986,11 +3818,10 @@ def users_get_user(user_id: str) -> str: response = client.users.get_user(user_id=user_id) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # VALIDATE - @mcp.tool() def validate_post_length(text: str) -> str: """Validate post character count @@ -3002,8 +3833,7 @@ def validate_post_length(text: str) -> str: response = client.validate.validate_post_length(text=text) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def validate_post(platforms: str, content: str = "", media_items: str = "") -> str: @@ -3015,11 +3845,12 @@ def validate_post(platforms: str, content: str = "", media_items: str = "") -> s media_items: Root media items shared across platforms""" client = _get_client() try: - response = client.validate.validate_post(content=content, platforms=platforms, media_items=media_items) + response = client.validate.validate_post( + content=content, platforms=platforms, media_items=media_items + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def validate_media(url: str) -> str: @@ -3032,8 +3863,7 @@ def validate_media(url: str) -> str: response = client.validate.validate_media(url=url) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def validate_subreddit(name: str) -> str: @@ -3046,11 +3876,10 @@ def validate_subreddit(name: str) -> str: response = client.validate.validate_subreddit(name=name) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # WEBHOOKS - @mcp.tool() def webhooks_get_webhook_settings() -> str: """List webhooks""" @@ -3059,11 +3888,17 @@ def webhooks_get_webhook_settings() -> str: response = client.webhooks.get_webhook_settings() return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def webhooks_create_webhook_settings(name: str = "", url: str = "", secret: str = "", events: str = "", is_active: bool = False, custom_headers: str = "") -> str: + def webhooks_create_webhook_settings( + name: str = "", + url: str = "", + secret: str = "", + events: str = "", + is_active: bool = False, + custom_headers: str = "", + ) -> str: """Create webhook Args: @@ -3075,14 +3910,28 @@ def webhooks_create_webhook_settings(name: str = "", url: str = "", secret: str custom_headers: Custom headers to include in webhook requests""" client = _get_client() try: - response = client.webhooks.create_webhook_settings(name=name, url=url, secret=secret, events=events, is_active=is_active, custom_headers=custom_headers) + response = client.webhooks.create_webhook_settings( + name=name, + url=url, + secret=secret, + events=events, + is_active=is_active, + custom_headers=custom_headers, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def webhooks_update_webhook_settings(id: str, name: str = "", url: str = "", secret: str = "", events: str = "", is_active: bool = False, custom_headers: str = "") -> str: + def webhooks_update_webhook_settings( + id: str, + name: str = "", + url: str = "", + secret: str = "", + events: str = "", + is_active: bool = False, + custom_headers: str = "", + ) -> str: """Update webhook Args: @@ -3095,11 +3944,18 @@ def webhooks_update_webhook_settings(id: str, name: str = "", url: str = "", sec custom_headers: Custom headers to include in webhook requests""" client = _get_client() try: - response = client.webhooks.update_webhook_settings(id=id, name=name, url=url, secret=secret, events=events, is_active=is_active, custom_headers=custom_headers) + response = client.webhooks.update_webhook_settings( + id=id, + name=name, + url=url, + secret=secret, + events=events, + is_active=is_active, + custom_headers=custom_headers, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def webhooks_delete_webhook_settings(id: str) -> str: @@ -3112,8 +3968,7 @@ def webhooks_delete_webhook_settings(id: str) -> str: response = client.webhooks.delete_webhook_settings(id=id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def webhooks_test_webhook(webhook_id: str) -> str: @@ -3126,11 +3981,12 @@ def webhooks_test_webhook(webhook_id: str) -> str: response = client.webhooks.test_webhook(webhook_id=webhook_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def webhooks_get_webhook_logs(limit: int = 50, status: str = "", event: str = "", webhook_id: str = "") -> str: + def webhooks_get_webhook_logs( + limit: int = 50, status: str = "", event: str = "", webhook_id: str = "" + ) -> str: """Get delivery logs Args: @@ -3140,16 +3996,19 @@ def webhooks_get_webhook_logs(limit: int = 50, status: str = "", event: str = "" webhook_id: Filter by webhook ID""" client = _get_client() try: - response = client.webhooks.get_webhook_logs(limit=limit, status=status, event=event, webhook_id=webhook_id) + response = client.webhooks.get_webhook_logs( + limit=limit, status=status, event=event, webhook_id=webhook_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # WHATSAPP - @mcp.tool() - def whatsapp_send_whats_app_bulk(account_id: str, recipients: str, template: str) -> str: + def whatsapp_send_whats_app_bulk( + account_id: str, recipients: str, template: str + ) -> str: """Bulk send template messages Args: @@ -3158,14 +4017,23 @@ def whatsapp_send_whats_app_bulk(account_id: str, recipients: str, template: str template: (required)""" client = _get_client() try: - response = client.whatsapp.send_whats_app_bulk(account_id=account_id, recipients=recipients, template=template) + response = client.whatsapp.send_whats_app_bulk( + account_id=account_id, recipients=recipients, template=template + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_get_whats_app_contacts(account_id: str, search: str = "", tag: str = "", group: str = "", opted_in: str = "", limit: int = 50, skip: int = 0) -> str: + def whatsapp_get_whats_app_contacts( + account_id: str, + search: str = "", + tag: str = "", + group: str = "", + opted_in: str = "", + limit: int = 50, + skip: int = 0, + ) -> str: """List contacts Args: @@ -3178,14 +4046,32 @@ def whatsapp_get_whats_app_contacts(account_id: str, search: str = "", tag: str skip: Offset for pagination""" client = _get_client() try: - response = client.whatsapp.get_whats_app_contacts(account_id=account_id, search=search, tag=tag, group=group, opted_in=opted_in, limit=limit, skip=skip) - return _format_response(response) - except Exception as e: - return f'Error: {e}' - - - @mcp.tool() - def whatsapp_create_whats_app_contact(account_id: str, phone: str, name: str, email: str = "", company: str = "", tags: str = "", groups: str = "", is_opted_in: bool = True, custom_fields: str = "", notes: str = "") -> str: + response = client.whatsapp.get_whats_app_contacts( + account_id=account_id, + search=search, + tag=tag, + group=group, + opted_in=opted_in, + limit=limit, + skip=skip, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def whatsapp_create_whats_app_contact( + account_id: str, + phone: str, + name: str, + email: str = "", + company: str = "", + tags: str = "", + groups: str = "", + is_opted_in: bool = True, + custom_fields: str = "", + notes: str = "", + ) -> str: """Create contact Args: @@ -3201,11 +4087,21 @@ def whatsapp_create_whats_app_contact(account_id: str, phone: str, name: str, em notes: Notes about the contact""" client = _get_client() try: - response = client.whatsapp.create_whats_app_contact(account_id=account_id, phone=phone, name=name, email=email, company=company, tags=tags, groups=groups, is_opted_in=is_opted_in, custom_fields=custom_fields, notes=notes) + response = client.whatsapp.create_whats_app_contact( + account_id=account_id, + phone=phone, + name=name, + email=email, + company=company, + tags=tags, + groups=groups, + is_opted_in=is_opted_in, + custom_fields=custom_fields, + notes=notes, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_contact(contact_id: str) -> str: @@ -3218,11 +4114,21 @@ def whatsapp_get_whats_app_contact(contact_id: str) -> str: response = client.whatsapp.get_whats_app_contact(contact_id=contact_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_update_whats_app_contact(contact_id: str, name: str = "", email: str = "", company: str = "", tags: str = "", groups: str = "", is_opted_in: bool = False, is_blocked: bool = False, custom_fields: str = "", notes: str = "") -> str: + def whatsapp_update_whats_app_contact( + contact_id: str, + name: str = "", + email: str = "", + company: str = "", + tags: str = "", + groups: str = "", + is_opted_in: bool = False, + is_blocked: bool = False, + custom_fields: str = "", + notes: str = "", + ) -> str: """Update contact Args: @@ -3238,11 +4144,21 @@ def whatsapp_update_whats_app_contact(contact_id: str, name: str = "", email: st notes: Notes about the contact""" client = _get_client() try: - response = client.whatsapp.update_whats_app_contact(contact_id=contact_id, name=name, email=email, company=company, tags=tags, groups=groups, is_opted_in=is_opted_in, is_blocked=is_blocked, custom_fields=custom_fields, notes=notes) + response = client.whatsapp.update_whats_app_contact( + contact_id=contact_id, + name=name, + email=email, + company=company, + tags=tags, + groups=groups, + is_opted_in=is_opted_in, + is_blocked=is_blocked, + custom_fields=custom_fields, + notes=notes, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_delete_whats_app_contact(contact_id: str) -> str: @@ -3255,11 +4171,16 @@ def whatsapp_delete_whats_app_contact(contact_id: str) -> str: response = client.whatsapp.delete_whats_app_contact(contact_id=contact_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_import_whats_app_contacts(account_id: str, contacts: str, default_tags: str = "", default_groups: str = "", skip_duplicates: bool = True) -> str: + def whatsapp_import_whats_app_contacts( + account_id: str, + contacts: str, + default_tags: str = "", + default_groups: str = "", + skip_duplicates: bool = True, + ) -> str: """Bulk import contacts Args: @@ -3270,14 +4191,21 @@ def whatsapp_import_whats_app_contacts(account_id: str, contacts: str, default_t skip_duplicates: Skip contacts with existing phone numbers""" client = _get_client() try: - response = client.whatsapp.import_whats_app_contacts(account_id=account_id, contacts=contacts, default_tags=default_tags, default_groups=default_groups, skip_duplicates=skip_duplicates) + response = client.whatsapp.import_whats_app_contacts( + account_id=account_id, + contacts=contacts, + default_tags=default_tags, + default_groups=default_groups, + skip_duplicates=skip_duplicates, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_bulk_update_whats_app_contacts(action: str, contact_ids: str, tags: str = "", groups: str = "") -> str: + def whatsapp_bulk_update_whats_app_contacts( + action: str, contact_ids: str, tags: str = "", groups: str = "" + ) -> str: """Bulk update contacts Args: @@ -3287,11 +4215,12 @@ def whatsapp_bulk_update_whats_app_contacts(action: str, contact_ids: str, tags: groups: Groups to add or remove (required for addGroups/removeGroups)""" client = _get_client() try: - response = client.whatsapp.bulk_update_whats_app_contacts(action=action, contact_ids=contact_ids, tags=tags, groups=groups) + response = client.whatsapp.bulk_update_whats_app_contacts( + action=action, contact_ids=contact_ids, tags=tags, groups=groups + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_bulk_delete_whats_app_contacts(contact_ids: str) -> str: @@ -3301,11 +4230,12 @@ def whatsapp_bulk_delete_whats_app_contacts(contact_ids: str) -> str: contact_ids: Contact IDs to delete (max 500) (required)""" client = _get_client() try: - response = client.whatsapp.bulk_delete_whats_app_contacts(contact_ids=contact_ids) + response = client.whatsapp.bulk_delete_whats_app_contacts( + contact_ids=contact_ids + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_groups(account_id: str) -> str: @@ -3318,11 +4248,12 @@ def whatsapp_get_whats_app_groups(account_id: str) -> str: response = client.whatsapp.get_whats_app_groups(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_rename_whats_app_group(account_id: str, old_name: str, new_name: str) -> str: + def whatsapp_rename_whats_app_group( + account_id: str, old_name: str, new_name: str + ) -> str: """Rename group Args: @@ -3331,11 +4262,12 @@ def whatsapp_rename_whats_app_group(account_id: str, old_name: str, new_name: st new_name: New group name (required)""" client = _get_client() try: - response = client.whatsapp.rename_whats_app_group(account_id=account_id, old_name=old_name, new_name=new_name) + response = client.whatsapp.rename_whats_app_group( + account_id=account_id, old_name=old_name, new_name=new_name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_delete_whats_app_group(account_id: str, group_name: str) -> str: @@ -3346,11 +4278,12 @@ def whatsapp_delete_whats_app_group(account_id: str, group_name: str) -> str: group_name: Group name to delete (required)""" client = _get_client() try: - response = client.whatsapp.delete_whats_app_group(account_id=account_id, group_name=group_name) + response = client.whatsapp.delete_whats_app_group( + account_id=account_id, group_name=group_name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_templates(account_id: str) -> str: @@ -3363,34 +4296,50 @@ def whatsapp_get_whats_app_templates(account_id: str) -> str: response = client.whatsapp.get_whats_app_templates(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_create_whats_app_template(account_id: str, name: str, category: str, language: str, components: str = "", library_template_name: str = "", library_template_body_inputs: str = "", library_template_button_inputs: str = "") -> str: + def whatsapp_create_whats_app_template( + account_id: str, + name: str, + category: str, + language: str, + components: str = "", + library_template_name: str = "", + library_template_body_inputs: str = "", + library_template_button_inputs: str = "", + ) -> str: """Create template - Args: - account_id: WhatsApp social account ID (required) - name: Template name (lowercase, letters/numbers/underscores, must start with a letter) (required) - category: Template category (required) - language: Template language code (e.g., en_US) (required) - components: Template components (header, body, footer, buttons). Required for custom templates, omit when using library_template_name. - library_template_name: Name of a pre-built template from Meta's template library (e.g., "appointment_reminder", - "auto_pay_reminder_1", "address_update"). When provided, the template is pre-approved - by Meta with no review wait. Omit `components` when using this field. - library_template_body_inputs: Optional body customizations for library templates. Available options depend on the - template (e.g., add_contact_number, add_learn_more_link, add_security_recommendation, - add_track_package_link, code_expiration_minutes). - library_template_button_inputs: Optional button customizations for library templates. Each item specifies button type - and configuration (e.g., URL, phone number, quick reply).""" - client = _get_client() - try: - response = client.whatsapp.create_whats_app_template(account_id=account_id, name=name, category=category, language=language, components=components, library_template_name=library_template_name, library_template_body_inputs=library_template_body_inputs, library_template_button_inputs=library_template_button_inputs) - return _format_response(response) - except Exception as e: - return f'Error: {e}' - + Args: + account_id: WhatsApp social account ID (required) + name: Template name (lowercase, letters/numbers/underscores, must start with a letter) (required) + category: Template category (required) + language: Template language code (e.g., en_US) (required) + components: Template components (header, body, footer, buttons). Required for custom templates, omit when using library_template_name. + library_template_name: Name of a pre-built template from Meta's template library (e.g., "appointment_reminder", + "auto_pay_reminder_1", "address_update"). When provided, the template is pre-approved + by Meta with no review wait. Omit `components` when using this field. + library_template_body_inputs: Optional body customizations for library templates. Available options depend on the + template (e.g., add_contact_number, add_learn_more_link, add_security_recommendation, + add_track_package_link, code_expiration_minutes). + library_template_button_inputs: Optional button customizations for library templates. Each item specifies button type + and configuration (e.g., URL, phone number, quick reply).""" + client = _get_client() + try: + response = client.whatsapp.create_whats_app_template( + account_id=account_id, + name=name, + category=category, + language=language, + components=components, + library_template_name=library_template_name, + library_template_body_inputs=library_template_body_inputs, + library_template_button_inputs=library_template_button_inputs, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_template(template_name: str, account_id: str) -> str: @@ -3401,14 +4350,17 @@ def whatsapp_get_whats_app_template(template_name: str, account_id: str) -> str: account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.get_whats_app_template(template_name=template_name, account_id=account_id) + response = client.whatsapp.get_whats_app_template( + template_name=template_name, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_update_whats_app_template(template_name: str, account_id: str, components: str) -> str: + def whatsapp_update_whats_app_template( + template_name: str, account_id: str, components: str + ) -> str: """Update template Args: @@ -3417,11 +4369,14 @@ def whatsapp_update_whats_app_template(template_name: str, account_id: str, comp components: Updated template components (required)""" client = _get_client() try: - response = client.whatsapp.update_whats_app_template(template_name=template_name, account_id=account_id, components=components) + response = client.whatsapp.update_whats_app_template( + template_name=template_name, + account_id=account_id, + components=components, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_delete_whats_app_template(template_name: str, account_id: str) -> str: @@ -3432,14 +4387,17 @@ def whatsapp_delete_whats_app_template(template_name: str, account_id: str) -> s account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.delete_whats_app_template(template_name=template_name, account_id=account_id) + response = client.whatsapp.delete_whats_app_template( + template_name=template_name, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_get_whats_app_broadcasts(account_id: str, status: str = "", limit: int = 50, skip: int = 0) -> str: + def whatsapp_get_whats_app_broadcasts( + account_id: str, status: str = "", limit: int = 50, skip: int = 0 + ) -> str: """List broadcasts Args: @@ -3449,14 +4407,21 @@ def whatsapp_get_whats_app_broadcasts(account_id: str, status: str = "", limit: skip: Offset for pagination""" client = _get_client() try: - response = client.whatsapp.get_whats_app_broadcasts(account_id=account_id, status=status, limit=limit, skip=skip) + response = client.whatsapp.get_whats_app_broadcasts( + account_id=account_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_create_whats_app_broadcast(account_id: str, name: str, template: str, description: str = "", recipients: str = "") -> str: + def whatsapp_create_whats_app_broadcast( + account_id: str, + name: str, + template: str, + description: str = "", + recipients: str = "", + ) -> str: """Create broadcast Args: @@ -3467,11 +4432,16 @@ def whatsapp_create_whats_app_broadcast(account_id: str, name: str, template: st recipients: Initial recipients (optional)""" client = _get_client() try: - response = client.whatsapp.create_whats_app_broadcast(account_id=account_id, name=name, description=description, template=template, recipients=recipients) + response = client.whatsapp.create_whats_app_broadcast( + account_id=account_id, + name=name, + description=description, + template=template, + recipients=recipients, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_broadcast(broadcast_id: str) -> str: @@ -3481,11 +4451,12 @@ def whatsapp_get_whats_app_broadcast(broadcast_id: str) -> str: broadcast_id: Broadcast ID (required)""" client = _get_client() try: - response = client.whatsapp.get_whats_app_broadcast(broadcast_id=broadcast_id) + response = client.whatsapp.get_whats_app_broadcast( + broadcast_id=broadcast_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_delete_whats_app_broadcast(broadcast_id: str) -> str: @@ -3495,11 +4466,12 @@ def whatsapp_delete_whats_app_broadcast(broadcast_id: str) -> str: broadcast_id: Broadcast ID (required)""" client = _get_client() try: - response = client.whatsapp.delete_whats_app_broadcast(broadcast_id=broadcast_id) + response = client.whatsapp.delete_whats_app_broadcast( + broadcast_id=broadcast_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_send_whats_app_broadcast(broadcast_id: str) -> str: @@ -3509,14 +4481,17 @@ def whatsapp_send_whats_app_broadcast(broadcast_id: str) -> str: broadcast_id: Broadcast ID (required)""" client = _get_client() try: - response = client.whatsapp.send_whats_app_broadcast(broadcast_id=broadcast_id) + response = client.whatsapp.send_whats_app_broadcast( + broadcast_id=broadcast_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_schedule_whats_app_broadcast(broadcast_id: str, scheduled_at: str) -> str: + def whatsapp_schedule_whats_app_broadcast( + broadcast_id: str, scheduled_at: str + ) -> str: """Schedule broadcast Args: @@ -3524,11 +4499,12 @@ def whatsapp_schedule_whats_app_broadcast(broadcast_id: str, scheduled_at: str) scheduled_at: ISO 8601 date-time for sending (must be in the future, max 30 days) (required)""" client = _get_client() try: - response = client.whatsapp.schedule_whats_app_broadcast(broadcast_id=broadcast_id, scheduled_at=scheduled_at) + response = client.whatsapp.schedule_whats_app_broadcast( + broadcast_id=broadcast_id, scheduled_at=scheduled_at + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_cancel_whats_app_broadcast_schedule(broadcast_id: str) -> str: @@ -3538,14 +4514,17 @@ def whatsapp_cancel_whats_app_broadcast_schedule(broadcast_id: str) -> str: broadcast_id: Broadcast ID (required)""" client = _get_client() try: - response = client.whatsapp.cancel_whats_app_broadcast_schedule(broadcast_id=broadcast_id) + response = client.whatsapp.cancel_whats_app_broadcast_schedule( + broadcast_id=broadcast_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_get_whats_app_broadcast_recipients(broadcast_id: str, status: str = "", limit: int = 100, skip: int = 0) -> str: + def whatsapp_get_whats_app_broadcast_recipients( + broadcast_id: str, status: str = "", limit: int = 100, skip: int = 0 + ) -> str: """List recipients Args: @@ -3555,14 +4534,17 @@ def whatsapp_get_whats_app_broadcast_recipients(broadcast_id: str, status: str = skip: Offset for pagination""" client = _get_client() try: - response = client.whatsapp.get_whats_app_broadcast_recipients(broadcast_id=broadcast_id, status=status, limit=limit, skip=skip) + response = client.whatsapp.get_whats_app_broadcast_recipients( + broadcast_id=broadcast_id, status=status, limit=limit, skip=skip + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_add_whats_app_broadcast_recipients(broadcast_id: str, recipients: str) -> str: + def whatsapp_add_whats_app_broadcast_recipients( + broadcast_id: str, recipients: str + ) -> str: """Add recipients Args: @@ -3570,14 +4552,17 @@ def whatsapp_add_whats_app_broadcast_recipients(broadcast_id: str, recipients: s recipients: Recipients to add (max 1000) (required)""" client = _get_client() try: - response = client.whatsapp.add_whats_app_broadcast_recipients(broadcast_id=broadcast_id, recipients=recipients) + response = client.whatsapp.add_whats_app_broadcast_recipients( + broadcast_id=broadcast_id, recipients=recipients + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_remove_whats_app_broadcast_recipients(broadcast_id: str, phones: str) -> str: + def whatsapp_remove_whats_app_broadcast_recipients( + broadcast_id: str, phones: str + ) -> str: """Remove recipients Args: @@ -3585,11 +4570,12 @@ def whatsapp_remove_whats_app_broadcast_recipients(broadcast_id: str, phones: st phones: Phone numbers to remove (required)""" client = _get_client() try: - response = client.whatsapp.remove_whats_app_broadcast_recipients(broadcast_id=broadcast_id, phones=phones) + response = client.whatsapp.remove_whats_app_broadcast_recipients( + broadcast_id=broadcast_id, phones=phones + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_business_profile(account_id: str) -> str: @@ -3599,14 +4585,24 @@ def whatsapp_get_whats_app_business_profile(account_id: str) -> str: account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.get_whats_app_business_profile(account_id=account_id) + response = client.whatsapp.get_whats_app_business_profile( + account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_update_whats_app_business_profile(account_id: str, about: str = "", address: str = "", description: str = "", email: str = "", websites: str = "", vertical: str = "", profile_picture_handle: str = "") -> str: + def whatsapp_update_whats_app_business_profile( + account_id: str, + about: str = "", + address: str = "", + description: str = "", + email: str = "", + websites: str = "", + vertical: str = "", + profile_picture_handle: str = "", + ) -> str: """Update business profile Args: @@ -3620,11 +4616,19 @@ def whatsapp_update_whats_app_business_profile(account_id: str, about: str = "", profile_picture_handle: Handle from resumable upload for profile picture""" client = _get_client() try: - response = client.whatsapp.update_whats_app_business_profile(account_id=account_id, about=about, address=address, description=description, email=email, websites=websites, vertical=vertical, profile_picture_handle=profile_picture_handle) + response = client.whatsapp.update_whats_app_business_profile( + account_id=account_id, + about=about, + address=address, + description=description, + email=email, + websites=websites, + vertical=vertical, + profile_picture_handle=profile_picture_handle, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_upload_whats_app_profile_photo() -> str: @@ -3634,8 +4638,7 @@ def whatsapp_upload_whats_app_profile_photo() -> str: response = client.whatsapp.upload_whats_app_profile_photo() return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_display_name(account_id: str) -> str: @@ -3648,11 +4651,12 @@ def whatsapp_get_whats_app_display_name(account_id: str) -> str: response = client.whatsapp.get_whats_app_display_name(account_id=account_id) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_update_whats_app_display_name(account_id: str, display_name: str) -> str: + def whatsapp_update_whats_app_display_name( + account_id: str, display_name: str + ) -> str: """Request display name change Args: @@ -3660,14 +4664,17 @@ def whatsapp_update_whats_app_display_name(account_id: str, display_name: str) - display_name: New display name (must follow WhatsApp naming guidelines) (required)""" client = _get_client() try: - response = client.whatsapp.update_whats_app_display_name(account_id=account_id, display_name=display_name) + response = client.whatsapp.update_whats_app_display_name( + account_id=account_id, display_name=display_name + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_list_whats_app_group_chats(account_id: str, limit: int = 25, after: str = "") -> str: + def whatsapp_list_whats_app_group_chats( + account_id: str, limit: int = 25, after: str = "" + ) -> str: """List active groups Args: @@ -3676,14 +4683,20 @@ def whatsapp_list_whats_app_group_chats(account_id: str, limit: int = 25, after: after: Pagination cursor""" client = _get_client() try: - response = client.whatsapp.list_whats_app_group_chats(account_id=account_id, limit=limit, after=after) + response = client.whatsapp.list_whats_app_group_chats( + account_id=account_id, limit=limit, after=after + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_create_whats_app_group_chat(account_id: str, subject: str, description: str = "", join_approval_mode: str = "") -> str: + def whatsapp_create_whats_app_group_chat( + account_id: str, + subject: str, + description: str = "", + join_approval_mode: str = "", + ) -> str: """Create group Args: @@ -3693,11 +4706,15 @@ def whatsapp_create_whats_app_group_chat(account_id: str, subject: str, descript join_approval_mode: Whether users need approval to join via invite link""" client = _get_client() try: - response = client.whatsapp.create_whats_app_group_chat(account_id=account_id, subject=subject, description=description, join_approval_mode=join_approval_mode) + response = client.whatsapp.create_whats_app_group_chat( + account_id=account_id, + subject=subject, + description=description, + join_approval_mode=join_approval_mode, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_get_whats_app_group_chat(group_id: str, account_id: str) -> str: @@ -3708,14 +4725,21 @@ def whatsapp_get_whats_app_group_chat(group_id: str, account_id: str) -> str: account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.get_whats_app_group_chat(group_id=group_id, account_id=account_id) + response = client.whatsapp.get_whats_app_group_chat( + group_id=group_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_update_whats_app_group_chat(group_id: str, account_id: str, subject: str = "", description: str = "", join_approval_mode: str = "") -> str: + def whatsapp_update_whats_app_group_chat( + group_id: str, + account_id: str, + subject: str = "", + description: str = "", + join_approval_mode: str = "", + ) -> str: """Update group settings Args: @@ -3726,11 +4750,16 @@ def whatsapp_update_whats_app_group_chat(group_id: str, account_id: str, subject join_approval_mode""" client = _get_client() try: - response = client.whatsapp.update_whats_app_group_chat(group_id=group_id, account_id=account_id, subject=subject, description=description, join_approval_mode=join_approval_mode) + response = client.whatsapp.update_whats_app_group_chat( + group_id=group_id, + account_id=account_id, + subject=subject, + description=description, + join_approval_mode=join_approval_mode, + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_delete_whats_app_group_chat(group_id: str, account_id: str) -> str: @@ -3741,14 +4770,17 @@ def whatsapp_delete_whats_app_group_chat(group_id: str, account_id: str) -> str: account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.delete_whats_app_group_chat(group_id=group_id, account_id=account_id) + response = client.whatsapp.delete_whats_app_group_chat( + group_id=group_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_add_whats_app_group_participants(group_id: str, account_id: str, phone_numbers: str) -> str: + def whatsapp_add_whats_app_group_participants( + group_id: str, account_id: str, phone_numbers: str + ) -> str: """Add participants Args: @@ -3757,14 +4789,17 @@ def whatsapp_add_whats_app_group_participants(group_id: str, account_id: str, ph phone_numbers: Phone numbers in E.164 format (max 8) (required)""" client = _get_client() try: - response = client.whatsapp.add_whats_app_group_participants(group_id=group_id, account_id=account_id, phone_numbers=phone_numbers) + response = client.whatsapp.add_whats_app_group_participants( + group_id=group_id, account_id=account_id, phone_numbers=phone_numbers + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_remove_whats_app_group_participants(group_id: str, account_id: str, phone_numbers: str) -> str: + def whatsapp_remove_whats_app_group_participants( + group_id: str, account_id: str, phone_numbers: str + ) -> str: """Remove participants Args: @@ -3773,14 +4808,17 @@ def whatsapp_remove_whats_app_group_participants(group_id: str, account_id: str, phone_numbers: Phone numbers to remove (required)""" client = _get_client() try: - response = client.whatsapp.remove_whats_app_group_participants(group_id=group_id, account_id=account_id, phone_numbers=phone_numbers) + response = client.whatsapp.remove_whats_app_group_participants( + group_id=group_id, account_id=account_id, phone_numbers=phone_numbers + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_create_whats_app_group_invite_link(group_id: str, account_id: str) -> str: + def whatsapp_create_whats_app_group_invite_link( + group_id: str, account_id: str + ) -> str: """Create invite link Args: @@ -3788,14 +4826,17 @@ def whatsapp_create_whats_app_group_invite_link(group_id: str, account_id: str) account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.create_whats_app_group_invite_link(group_id=group_id, account_id=account_id) + response = client.whatsapp.create_whats_app_group_invite_link( + group_id=group_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_list_whats_app_group_join_requests(group_id: str, account_id: str) -> str: + def whatsapp_list_whats_app_group_join_requests( + group_id: str, account_id: str + ) -> str: """List join requests Args: @@ -3803,14 +4844,17 @@ def whatsapp_list_whats_app_group_join_requests(group_id: str, account_id: str) account_id: WhatsApp social account ID (required)""" client = _get_client() try: - response = client.whatsapp.list_whats_app_group_join_requests(group_id=group_id, account_id=account_id) + response = client.whatsapp.list_whats_app_group_join_requests( + group_id=group_id, account_id=account_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_approve_whats_app_group_join_requests(group_id: str, account_id: str, phone_numbers: str) -> str: + def whatsapp_approve_whats_app_group_join_requests( + group_id: str, account_id: str, phone_numbers: str + ) -> str: """Approve join requests Args: @@ -3819,14 +4863,17 @@ def whatsapp_approve_whats_app_group_join_requests(group_id: str, account_id: st phone_numbers: Phone numbers to approve (required)""" client = _get_client() try: - response = client.whatsapp.approve_whats_app_group_join_requests(group_id=group_id, account_id=account_id, phone_numbers=phone_numbers) + response = client.whatsapp.approve_whats_app_group_join_requests( + group_id=group_id, account_id=account_id, phone_numbers=phone_numbers + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_reject_whats_app_group_join_requests(group_id: str, account_id: str, phone_numbers: str) -> str: + def whatsapp_reject_whats_app_group_join_requests( + group_id: str, account_id: str, phone_numbers: str + ) -> str: """Reject join requests Args: @@ -3835,16 +4882,19 @@ def whatsapp_reject_whats_app_group_join_requests(group_id: str, account_id: str phone_numbers: Phone numbers to reject (required)""" client = _get_client() try: - response = client.whatsapp.reject_whats_app_group_join_requests(group_id=group_id, account_id=account_id, phone_numbers=phone_numbers) + response = client.whatsapp.reject_whats_app_group_join_requests( + group_id=group_id, account_id=account_id, phone_numbers=phone_numbers + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" # WHATSAPP_PHONE_NUMBERS - @mcp.tool() - def whatsapp_phone_numbers_get_whats_app_phone_numbers(status: str = "", profile_id: str = "") -> str: + def whatsapp_phone_numbers_get_whats_app_phone_numbers( + status: str = "", profile_id: str = "" + ) -> str: """List phone numbers Args: @@ -3852,11 +4902,12 @@ def whatsapp_phone_numbers_get_whats_app_phone_numbers(status: str = "", profile profile_id: Filter by profile""" client = _get_client() try: - response = client.whatsapp_phone_numbers.get_whats_app_phone_numbers(status=status, profile_id=profile_id) + response = client.whatsapp_phone_numbers.get_whats_app_phone_numbers( + status=status, profile_id=profile_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_phone_numbers_purchase_whats_app_phone_number(profile_id: str) -> str: @@ -3866,11 +4917,12 @@ def whatsapp_phone_numbers_purchase_whats_app_phone_number(profile_id: str) -> s profile_id: Profile to associate the number with (required)""" client = _get_client() try: - response = client.whatsapp_phone_numbers.purchase_whats_app_phone_number(profile_id=profile_id) + response = client.whatsapp_phone_numbers.purchase_whats_app_phone_number( + profile_id=profile_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() def whatsapp_phone_numbers_get_whats_app_phone_number(phone_number_id: str) -> str: @@ -3880,21 +4932,26 @@ def whatsapp_phone_numbers_get_whats_app_phone_number(phone_number_id: str) -> s phone_number_id: Phone number record ID (required)""" client = _get_client() try: - response = client.whatsapp_phone_numbers.get_whats_app_phone_number(phone_number_id=phone_number_id) + response = client.whatsapp_phone_numbers.get_whats_app_phone_number( + phone_number_id=phone_number_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' - + return f"Error: {e}" @mcp.tool() - def whatsapp_phone_numbers_release_whats_app_phone_number(phone_number_id: str) -> str: + def whatsapp_phone_numbers_release_whats_app_phone_number( + phone_number_id: str, + ) -> str: """Release phone number Args: phone_number_id: Phone number record ID (required)""" client = _get_client() try: - response = client.whatsapp_phone_numbers.release_whats_app_phone_number(phone_number_id=phone_number_id) + response = client.whatsapp_phone_numbers.release_whats_app_phone_number( + phone_number_id=phone_number_id + ) return _format_response(response) except Exception as e: - return f'Error: {e}' + return f"Error: {e}" diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index f9a864e..9814395 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-26T15:28:29+00:00 +# timestamp: 2026-03-26T18:00:11+00:00 from __future__ import annotations diff --git a/src/late/resources/_generated/posts.py b/src/late/resources/_generated/posts.py index 5573952..c2bb3dc 100644 --- a/src/late/resources/_generated/posts.py +++ b/src/late/resources/_generated/posts.py @@ -173,6 +173,8 @@ def update_post_metadata( post_id: str, platform: str, *, + video_id: str | None = None, + account_id: str | None = None, title: str | None = None, description: str | None = None, tags: list[str] | None = None, @@ -182,6 +184,8 @@ def update_post_metadata( """Update post metadata""" payload = self._build_payload( platform=platform, + video_id=video_id, + account_id=account_id, title=title, description=description, tags=tags, @@ -315,6 +319,8 @@ async def aupdate_post_metadata( post_id: str, platform: str, *, + video_id: str | None = None, + account_id: str | None = None, title: str | None = None, description: str | None = None, tags: list[str] | None = None, @@ -324,6 +330,8 @@ async def aupdate_post_metadata( """Update post metadata (async)""" payload = self._build_payload( platform=platform, + video_id=video_id, + account_id=account_id, title=title, description=description, tags=tags, diff --git a/uv.lock b/uv.lock index 6a8b4ed..b69411a 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.25" +version = "1.3.27" source = { editable = "." } dependencies = [ { name = "httpx" }, From 1e4dff874bd4afc4a1f02cba026701c256433163 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 14:51:14 +0000 Subject: [PATCH 02/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.29 --- openapi.yaml | 256 ++++++++++++++++++++++++--- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/models/_generated/models.py | 208 ++++++++++++---------- uv.lock | 2 +- 5 files changed, 358 insertions(+), 112 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 8f66556..81112b6 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -126,15 +126,17 @@ x-documentation: Receive real-time notifications for post status changes, account events, and incoming messages: - - `post.scheduled` - Post successfully scheduled + - `post.scheduled` - Post created and scheduled for publishing - `post.published` - Post successfully published - `post.failed` - Post failed on all platforms - `post.partial` - Post published to some platforms, failed on others - - `post.recycled` - A new scheduled copy was created from a recycling post + - `post.cancelled` - Post publishing was cancelled + - `post.recycled` - Post recycled (cloned and re-scheduled) - `account.connected` - Social account connected - `account.disconnected` - Social account disconnected (token expired) - `message.received` - New DM received - `comment.received` - New comment received on a post + - `webhook.test` - Test event sent when verifying a webhook endpoint Webhook payloads are signed with HMAC-SHA256 via the `X-Zernio-Signature` header. @@ -233,8 +235,11 @@ tags: auto-creates contacts. - name: Webhooks description: | - Configure webhooks for real-time notifications. Events: post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received. + Configure webhooks for real-time notifications. Events: post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test. Security: optional HMAC-SHA256 signature in X-Zernio-Signature header. Configure a secret key to enable verification. Custom headers supported. + - name: Webhook Events + description: | + Incoming webhook deliveries sent by Zernio to your configured endpoint URL. - name: Logs description: | Publishing logs for transparency and debugging. Each log includes the platform API endpoint, HTTP status code, request/response bodies, duration, and retry attempts. Logs are automatically deleted after 7 days. @@ -586,7 +591,7 @@ components: type: array items: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received] description: Events subscribed to isActive: type: boolean @@ -617,7 +622,7 @@ components: description: Name of the webhook that was triggered event: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test] url: type: string format: uri @@ -648,12 +653,17 @@ components: WebhookPayloadPost: type: object description: Webhook payload for post events + required: [id, event, post, timestamp] properties: + id: + type: string + description: Stable webhook event ID event: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled] post: type: object + required: [id, content, status, scheduledFor, platforms] properties: id: type: string @@ -671,11 +681,14 @@ components: type: array items: type: object + required: [platform, status] properties: platform: type: string status: type: string + platformPostId: + type: string publishedUrl: type: string error: @@ -686,12 +699,17 @@ components: WebhookPayloadAccountConnected: type: object description: Webhook payload for account connected events + required: [id, event, account, timestamp] properties: + id: + type: string + description: Stable webhook event ID event: type: string enum: [account.connected] account: type: object + required: [accountId, profileId, platform, username] properties: accountId: type: string @@ -711,12 +729,17 @@ components: WebhookPayloadAccountDisconnected: type: object description: Webhook payload for account disconnected events + required: [id, event, account, timestamp] properties: + id: + type: string + description: Stable webhook event ID event: type: string enum: [account.disconnected] account: type: object + required: [accountId, profileId, platform, username, disconnectionType, reason] properties: accountId: type: string @@ -743,12 +766,17 @@ components: WebhookPayloadComment: type: object description: Webhook payload for comment received events (Instagram, Facebook, Twitter/X, YouTube, LinkedIn, Bluesky, Reddit) + required: [id, event, comment, post, account, timestamp] properties: + id: + type: string + description: Stable webhook event ID event: type: string enum: [comment.received] comment: type: object + required: [id, postId, platformPostId, platform, text, author, createdAt, isReply, parentCommentId] properties: id: type: string @@ -767,6 +795,7 @@ components: description: Comment text content author: type: object + required: [id] properties: id: type: string @@ -790,6 +819,7 @@ components: description: Parent comment ID if this is a reply post: type: object + required: [id, platformPostId] properties: id: type: string @@ -799,6 +829,7 @@ components: description: Platform's post ID account: type: object + required: [id, platform, username] properties: id: type: string @@ -812,13 +843,18 @@ components: format: date-time WebhookPayloadMessage: type: object - description: Webhook payload for message received events (DMs from Instagram, Facebook, Telegram, Bluesky, Reddit) + description: Webhook payload for message received events + required: [id, event, message, conversation, account, timestamp] properties: + id: + type: string + description: Stable webhook event ID event: type: string enum: [message.received] message: type: object + required: [id, conversationId, platform, platformMessageId, direction, text, attachments, sender, sentAt, isRead] properties: id: type: string @@ -828,13 +864,13 @@ components: description: Internal conversation ID platform: type: string - enum: [instagram, facebook, telegram, bluesky, reddit] + enum: [instagram, facebook, telegram, whatsapp] platformMessageId: type: string description: Platform's message ID direction: type: string - enum: [incoming] + enum: [incoming, outgoing] text: type: string nullable: true @@ -843,6 +879,7 @@ components: type: array items: type: object + required: [type, url] properties: type: type: string @@ -855,6 +892,7 @@ components: description: Additional attachment metadata sender: type: object + required: [id] properties: id: type: string @@ -866,7 +904,6 @@ components: type: string instagramProfile: type: object - nullable: true description: Instagram profile data for the sender. Only present for Instagram conversations. properties: isFollower: @@ -892,6 +929,7 @@ components: type: boolean conversation: type: object + required: [id, platformConversationId, status] properties: id: type: string @@ -905,16 +943,12 @@ components: type: string participantPicture: type: string - participantVerifiedType: - type: string - nullable: true - enum: [blue, government, business, none] - description: X/Twitter verified badge type. Only present for Twitter/X conversations. status: type: string enum: [active, archived] account: type: object + required: [id, platform, username] properties: id: type: string @@ -945,6 +979,23 @@ components: timestamp: type: string format: date-time + WebhookPayloadTest: + type: object + description: Webhook payload for test deliveries + required: [id, event, message, timestamp] + properties: + id: + type: string + description: Stable webhook event ID + event: + type: string + enum: [webhook.test] + message: + type: string + description: Human-readable test message + timestamp: + type: string + format: date-time PostLog: type: object description: Publishing log entry showing details of a post publishing attempt @@ -2413,6 +2464,172 @@ components: properties: user: $ref: '#/components/schemas/User' +webhooks: + post.scheduled: + post: + operationId: onPostScheduled + summary: Post scheduled event + description: Fired when a post is created and scheduled for publishing. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + post.published: + post: + operationId: onPostPublished + summary: Post published event + description: Fired when a post is successfully published. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + post.failed: + post: + operationId: onPostFailed + summary: Post failed event + description: Fired when a post fails to publish on all target platforms. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + post.partial: + post: + operationId: onPostPartial + summary: Post partial event + description: Fired when a post publishes on some platforms and fails on others. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + post.cancelled: + post: + operationId: onPostCancelled + summary: Post cancelled event + description: Fired when a post publishing job is cancelled. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + post.recycled: + post: + operationId: onPostRecycled + summary: Post recycled event + description: Fired when a post is recycled (cloned and re-scheduled for publishing). + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadPost' + responses: + '200': + description: Webhook received successfully + account.connected: + post: + operationId: onAccountConnected + summary: Account connected event + description: Fired when a social account is successfully connected. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadAccountConnected' + responses: + '200': + description: Webhook received successfully + account.disconnected: + post: + operationId: onAccountDisconnected + summary: Account disconnected event + description: Fired when a connected social account becomes disconnected. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadAccountDisconnected' + responses: + '200': + description: Webhook received successfully + message.received: + post: + operationId: onMessageReceived + summary: Message received event + description: Fired when a new inbox message is received. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadMessage' + responses: + '200': + description: Webhook received successfully + comment.received: + post: + operationId: onCommentReceived + summary: Comment received event + description: Fired when a new comment is received on a tracked post. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadComment' + responses: + '200': + description: Webhook received successfully + webhook.test: + post: + operationId: onWebhookTest + summary: Webhook test event + description: Fired when sending a test webhook to verify the endpoint configuration. + tags: [Webhook Events] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookPayloadTest' + responses: + '200': + description: Webhook received successfully security: - bearerAuth: [] paths: @@ -10305,7 +10522,7 @@ paths: type: array items: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received] description: Events to subscribe to isActive: type: boolean @@ -10322,7 +10539,7 @@ paths: name: "My Production Webhook" url: "https://example.com/webhook" secret: "your-secret-key" - events: ["post.scheduled", "post.published", "post.failed", "post.partial", "account.connected", "account.disconnected", "message.received", "comment.received"] + events: ["post.scheduled", "post.published", "post.failed", "post.partial", "post.cancelled", "post.recycled", "account.connected", "account.disconnected", "message.received", "comment.received"] isActive: true responses: '200': @@ -10374,7 +10591,7 @@ paths: type: array items: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received] description: Events to subscribe to isActive: type: boolean @@ -10516,7 +10733,7 @@ paths: description: Filter by event type schema: type: string - enum: [post.scheduled, post.published, post.failed, post.partial, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test] + enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test] - name: webhookId in: query description: Filter by webhook ID @@ -16472,4 +16689,3 @@ paths: hasMore: { type: boolean } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } - diff --git a/pyproject.toml b/pyproject.toml index f326b88..ef8fdcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.28" +version = "1.3.29" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 9375d65..d827ac8 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.28" +__version__ = "1.3.29" __all__ = [ # Client diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 9814395..ab98e97 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-26T18:00:11+00:00 +# timestamp: 2026-03-27T14:51:05+00:00 from __future__ import annotations @@ -289,6 +289,7 @@ class Event(Enum): POST_PUBLISHED = "post.published" POST_FAILED = "post.failed" POST_PARTIAL = "post.partial" + POST_CANCELLED = "post.cancelled" POST_RECYCLED = "post.recycled" ACCOUNT_CONNECTED = "account.connected" ACCOUNT_DISCONNECTED = "account.disconnected" @@ -344,6 +345,7 @@ class Event1(Enum): POST_PUBLISHED = "post.published" POST_FAILED = "post.failed" POST_PARTIAL = "post.partial" + POST_CANCELLED = "post.cancelled" POST_RECYCLED = "post.recycled" ACCOUNT_CONNECTED = "account.connected" ACCOUNT_DISCONNECTED = "account.disconnected" @@ -406,23 +408,25 @@ class Event2(Enum): POST_PUBLISHED = "post.published" POST_FAILED = "post.failed" POST_PARTIAL = "post.partial" + POST_CANCELLED = "post.cancelled" POST_RECYCLED = "post.recycled" class Platform(BaseModel): - platform: str | None = None - status: str | None = None + platform: str + status: str + platformPostId: str | None = None publishedUrl: str | None = None error: str | None = None class Post(BaseModel): - id: str | None = None - content: str | None = None - status: str | None = None - scheduledFor: AwareDatetime | None = None + id: str + content: str + status: str + scheduledFor: AwareDatetime publishedAt: AwareDatetime | None = None - platforms: List[Platform] | None = None + platforms: List[Platform] class WebhookPayloadPost(BaseModel): @@ -430,9 +434,13 @@ class WebhookPayloadPost(BaseModel): Webhook payload for post events """ - event: Event2 | None = None - post: Post | None = None - timestamp: AwareDatetime | None = None + id: str + """ + Stable webhook event ID + """ + event: Event2 + post: Post + timestamp: AwareDatetime class Event3(Enum): @@ -440,16 +448,16 @@ class Event3(Enum): class Account(BaseModel): - accountId: str | None = None + accountId: str """ The account's unique identifier (same as used in /v1/accounts/{accountId}) """ - profileId: str | None = None + profileId: str """ The profile's unique identifier this account belongs to """ - platform: str | None = None - username: str | None = None + platform: str + username: str displayName: str | None = None @@ -458,9 +466,13 @@ class WebhookPayloadAccountConnected(BaseModel): Webhook payload for account connected events """ - event: Event3 | None = None - account: Account | None = None - timestamp: AwareDatetime | None = None + id: str + """ + Stable webhook event ID + """ + event: Event3 + account: Account + timestamp: AwareDatetime class Event4(Enum): @@ -477,22 +489,22 @@ class DisconnectionType(Enum): class Account1(BaseModel): - accountId: str | None = None + accountId: str """ The account's unique identifier (same as used in /v1/accounts/{accountId}) """ - profileId: str | None = None + profileId: str """ The profile's unique identifier this account belongs to """ - platform: str | None = None - username: str | None = None + platform: str + username: str displayName: str | None = None - disconnectionType: DisconnectionType | None = None + disconnectionType: DisconnectionType """ Whether the disconnection was intentional (user action) or unintentional (token expired/revoked) """ - reason: str | None = None + reason: str """ Human-readable reason for the disconnection """ @@ -503,9 +515,13 @@ class WebhookPayloadAccountDisconnected(BaseModel): Webhook payload for account disconnected events """ - event: Event4 | None = None - account: Account1 | None = None - timestamp: AwareDatetime | None = None + id: str + """ + Stable webhook event ID + """ + event: Event4 + account: Account1 + timestamp: AwareDatetime class Event5(Enum): @@ -523,7 +539,7 @@ class Platform1(Enum): class Author(BaseModel): - id: str | None = None + id: str """ Author's platform ID """ @@ -533,53 +549,53 @@ class Author(BaseModel): class Comment(BaseModel): - id: str | None = None + id: str """ Platform comment ID """ - postId: str | None = None + postId: str """ Internal post ID """ - platformPostId: str | None = None + platformPostId: str """ Platform's post ID """ - platform: Platform1 | None = None - text: str | None = None + platform: Platform1 + text: str """ Comment text content """ - author: Author | None = None - createdAt: AwareDatetime | None = None - isReply: bool | None = None + author: Author + createdAt: AwareDatetime + isReply: bool """ Whether this is a reply to another comment """ - parentCommentId: str | None = None + parentCommentId: Annotated[str | None, Field(...)] = None """ Parent comment ID if this is a reply """ class Post1(BaseModel): - id: str | None = None + id: str """ Internal post ID """ - platformPostId: str | None = None + platformPostId: str """ Platform's post ID """ class Account2(BaseModel): - id: str | None = None + id: str """ Social account ID """ - platform: str | None = None - username: str | None = None + platform: str + username: str class WebhookPayloadComment(BaseModel): @@ -587,11 +603,15 @@ class WebhookPayloadComment(BaseModel): Webhook payload for comment received events (Instagram, Facebook, Twitter/X, YouTube, LinkedIn, Bluesky, Reddit) """ - event: Event5 | None = None - comment: Comment | None = None - post: Post1 | None = None - account: Account2 | None = None - timestamp: AwareDatetime | None = None + id: str + """ + Stable webhook event ID + """ + event: Event5 + comment: Comment + post: Post1 + account: Account2 + timestamp: AwareDatetime class Event6(Enum): @@ -602,20 +622,20 @@ class Platform2(Enum): INSTAGRAM = "instagram" FACEBOOK = "facebook" TELEGRAM = "telegram" - BLUESKY = "bluesky" - REDDIT = "reddit" + WHATSAPP = "whatsapp" class Direction(Enum): INCOMING = "incoming" + OUTGOING = "outgoing" class Attachment(BaseModel): - type: str | None = None + type: str """ Attachment type (image, video, file, sticker, audio) """ - url: str | None = None + url: str """ Attachment URL (may expire for Meta platforms) """ @@ -649,7 +669,7 @@ class InstagramProfile(BaseModel): class Sender(BaseModel): - id: str | None = None + id: str name: str | None = None username: str | None = None picture: str | None = None @@ -660,39 +680,28 @@ class Sender(BaseModel): class Message(BaseModel): - id: str | None = None + id: str """ Internal message ID """ - conversationId: str | None = None + conversationId: str """ Internal conversation ID """ - platform: Platform2 | None = None - platformMessageId: str | None = None + platform: Platform2 + platformMessageId: str """ Platform's message ID """ - direction: Direction | None = None - text: str | None = None + direction: Direction + text: Annotated[str | None, Field(...)] = None """ Message text content """ - attachments: List[Attachment] | None = None - sender: Sender | None = None - sentAt: AwareDatetime | None = None - isRead: bool | None = None - - -class ParticipantVerifiedType(Enum): - """ - X/Twitter verified badge type. Only present for Twitter/X conversations. - """ - - BLUE = "blue" - GOVERNMENT = "government" - BUSINESS = "business" - NONE = "none" + attachments: List[Attachment] + sender: Sender + sentAt: AwareDatetime + isRead: bool class Status1(Enum): @@ -701,26 +710,22 @@ class Status1(Enum): class Conversation(BaseModel): - id: str | None = None - platformConversationId: str | None = None + id: str + platformConversationId: str participantId: str | None = None participantName: str | None = None participantUsername: str | None = None participantPicture: str | None = None - participantVerifiedType: ParticipantVerifiedType | None = None - """ - X/Twitter verified badge type. Only present for Twitter/X conversations. - """ - status: Status1 | None = None + status: Status1 class Account3(BaseModel): - id: str | None = None + id: str """ Social account ID """ - platform: str | None = None - username: str | None = None + platform: str + username: str displayName: str | None = None @@ -749,18 +754,43 @@ class Metadata(BaseModel): class WebhookPayloadMessage(BaseModel): """ - Webhook payload for message received events (DMs from Instagram, Facebook, Telegram, Bluesky, Reddit) + Webhook payload for message received events """ - event: Event6 | None = None - message: Message | None = None - conversation: Conversation | None = None - account: Account3 | None = None + id: str + """ + Stable webhook event ID + """ + event: Event6 + message: Message + conversation: Conversation + account: Account3 metadata: Metadata | None = None """ Interactive message metadata (present when message is a quick reply tap, postback button tap, or inline keyboard callback) """ - timestamp: AwareDatetime | None = None + timestamp: AwareDatetime + + +class Event7(Enum): + WEBHOOK_TEST = "webhook.test" + + +class WebhookPayloadTest(BaseModel): + """ + Webhook payload for test deliveries + """ + + id: str + """ + Stable webhook event ID + """ + event: Event7 + message: str + """ + Human-readable test message + """ + timestamp: AwareDatetime class PostId(BaseModel): diff --git a/uv.lock b/uv.lock index b69411a..c288900 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.27" +version = "1.3.28" source = { editable = "." } dependencies = [ { name = "httpx" }, From 0538dc92167336e5e8703dc003d849aadb4cc2ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 15:11:54 +0000 Subject: [PATCH 03/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.30 --- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/models/_generated/models.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef8fdcc..b759053 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.29" +version = "1.3.30" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index d827ac8..7458782 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.29" +__version__ = "1.3.30" __all__ = [ # Client diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index ab98e97..d6931ce 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-27T14:51:05+00:00 +# timestamp: 2026-03-27T15:11:44+00:00 from __future__ import annotations diff --git a/uv.lock b/uv.lock index c288900..b70cd37 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.28" +version = "1.3.29" source = { editable = "." } dependencies = [ { name = "httpx" }, From 6e83f27fd282df4a451e79705f7428734b5f2002 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:14:31 +0100 Subject: [PATCH 04/17] fix: use kebab-case skip-existing for pypa/gh-action-pypi-publish --- .github/workflows/generate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 8d9f6c1..3f697d2 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -159,7 +159,7 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} - skip_existing: true + skip-existing: true - name: Swap package name to late-sdk if: steps.changes.outputs.has_changes == 'true' @@ -185,4 +185,4 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} - skip_existing: true + skip-existing: true From 60cd1d831d2e9a0b9da400242914c53900633e23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Mar 2026 13:17:10 +0000 Subject: [PATCH 05/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.31 --- openapi.yaml | 7 ++++++- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/models/_generated/models.py | 2 +- uv.lock | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 81112b6..4b96dfe 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11072,6 +11072,8 @@ paths: description: | Fetch conversations (DMs) from all connected messaging accounts in a single API call. Supports filtering by profile and platform. Results are aggregated and deduplicated. Supported platforms: Facebook, Instagram, Twitter/X, Bluesky, Reddit, Telegram. + + **Twitter/X limitation:** X has replaced traditional DMs with encrypted "X Chat" for many accounts. Messages sent or received through encrypted X Chat are not accessible via X's API (the `/2/dm_events` endpoint only returns legacy unencrypted DMs). This means some Twitter/X conversations may show only outgoing messages or appear empty. This is an X platform limitation that affects all third-party applications. See [X's docs on encrypted messaging](https://help.x.com/en/using-x/about-chat) for more details. tags: [Messages] security: [{ bearerAuth: [] }] parameters: @@ -11316,7 +11318,10 @@ paths: get: operationId: getInboxConversationMessages summary: List messages - description: Fetch messages for a specific conversation. Requires accountId query parameter. + description: | + Fetch messages for a specific conversation. Requires accountId query parameter. + + **Twitter/X limitation:** X's encrypted "X Chat" messages are not accessible via the API. Conversations where the other participant uses encrypted X Chat may only show your outgoing messages. See the [list conversations endpoint](#/Messages/listInboxConversations) for more details. tags: [Messages] security: [{ bearerAuth: [] }] parameters: diff --git a/pyproject.toml b/pyproject.toml index b759053..c24ef42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.30" +version = "1.3.31" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 7458782..5b59943 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.30" +__version__ = "1.3.31" __all__ = [ # Client diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index d6931ce..85d34e8 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-27T15:11:44+00:00 +# timestamp: 2026-03-29T13:17:02+00:00 from __future__ import annotations diff --git a/uv.lock b/uv.lock index b70cd37..27524f7 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.29" +version = "1.3.30" source = { editable = "." } dependencies = [ { name = "httpx" }, From 5c8f3905d460fe9d852e88ffc95fbc3de6f7b541 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Mar 2026 16:53:02 +0000 Subject: [PATCH 06/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.32 --- README.md | 5 + openapi.yaml | 224 ++++++++++++++++++++++ pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/mcp/generated_tools.py | 91 +++++++++ src/late/models/_generated/models.py | 2 +- src/late/resources/_generated/messages.py | 104 ++++++++++ uv.lock | 2 +- 8 files changed, 428 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e941e49..98569ee 100644 --- a/README.md +++ b/README.md @@ -455,8 +455,13 @@ Both `from zernio import ...` and `from late import ...` work identically. The ` | `messages.get_inbox_conversation()` | Get conversation | | `messages.get_inbox_conversation_messages()` | List messages | | `messages.update_inbox_conversation()` | Update conversation status | +| `messages.delete_inbox_message()` | Delete message | +| `messages.add_message_reaction()` | Add reaction | | `messages.edit_inbox_message()` | Edit message | +| `messages.remove_message_reaction()` | Remove reaction | | `messages.send_inbox_message()` | Send message | +| `messages.send_typing_indicator()` | Send typing indicator | +| `messages.upload_media_direct()` | Upload media file | ### Reviews (Inbox) | Method | Description | diff --git a/openapi.yaml b/openapi.yaml index 4b96dfe..53e1d63 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11610,6 +11610,230 @@ paths: '401': { $ref: '#/components/responses/Unauthorized' } '403': description: Inbox addon required + delete: + operationId: deleteInboxMessage + summary: Delete message + description: | + Delete a message from a conversation. Platform support varies: + - **Telegram**: Full delete (bot's own messages anytime, others if admin) + - **X/Twitter**: Full delete (own DM events only) + - **Bluesky**: Delete for self only (recipient still sees it) + - **Reddit**: Delete from sender's view only + - **Facebook, Instagram, WhatsApp**: Not supported (returns 400) + tags: [Messages] + security: [{ bearerAuth: [] }] + parameters: + - name: conversationId + in: path + required: true + schema: { type: string } + description: The conversation ID + - name: messageId + in: path + required: true + schema: { type: string } + description: The platform message ID to delete + - name: accountId + in: query + required: true + schema: { type: string } + description: Social account ID + responses: + '200': + description: Message deleted + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + '400': + description: Platform does not support deletion or invalid request + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Inbox addon required + '404': + description: Account or conversation not found + + /v1/inbox/conversations/{conversationId}/typing: + post: + operationId: sendTypingIndicator + summary: Send typing indicator + description: | + Show a typing indicator in a conversation. Platform support: + - **Facebook Messenger**: Shows "Page is typing..." for 20 seconds + - **Telegram**: Shows "Bot is typing..." for 5 seconds + - **All others**: Returns 200 but no-op (platform doesn't support it) + + Typing indicators are best-effort. The endpoint always returns 200 even if the platform call fails. + tags: [Messages] + security: [{ bearerAuth: [] }] + parameters: + - name: conversationId + in: path + required: true + schema: { type: string } + description: The conversation ID + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [accountId] + properties: + accountId: { type: string, description: Social account ID } + responses: + '200': + description: Typing indicator sent (or no-op on unsupported platforms) + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Inbox addon required + '404': + description: Account or conversation not found + + /v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions: + post: + operationId: addMessageReaction + summary: Add reaction + description: | + Add an emoji reaction to a message. Platform support: + - **Telegram**: Supports a subset of Unicode emoji reactions + - **WhatsApp**: Supports any standard emoji (one reaction per message per sender) + - **All others**: Returns 400 (not supported) + tags: [Messages] + security: [{ bearerAuth: [] }] + parameters: + - name: conversationId + in: path + required: true + schema: { type: string } + description: The conversation ID + - name: messageId + in: path + required: true + schema: { type: string } + description: The platform message ID to react to + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [accountId, emoji] + properties: + accountId: { type: string, description: Social account ID } + emoji: { type: string, description: 'Emoji character (e.g. "👍", "❤️")', example: '👍' } + responses: + '200': + description: Reaction added + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + '400': + description: Platform does not support reactions or invalid request + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Inbox addon required + '404': + description: Account or conversation not found + delete: + operationId: removeMessageReaction + summary: Remove reaction + description: | + Remove a reaction from a message. Platform support: + - **Telegram**: Send empty reaction array to clear + - **WhatsApp**: Send empty emoji to remove + - **All others**: Returns 400 (not supported) + tags: [Messages] + security: [{ bearerAuth: [] }] + parameters: + - name: conversationId + in: path + required: true + schema: { type: string } + description: The conversation ID + - name: messageId + in: path + required: true + schema: { type: string } + description: The platform message ID + - name: accountId + in: query + required: true + schema: { type: string } + description: Social account ID + responses: + '200': + description: Reaction removed + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + '400': + description: Platform does not support reactions or invalid request + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Inbox addon required + '404': + description: Account or conversation not found + + /v1/media/upload-direct: + post: + operationId: uploadMediaDirect + summary: Upload media file + description: | + Upload a media file using API key authentication and get back a publicly accessible URL. + The URL can be used as `attachmentUrl` when sending inbox messages. + + Files are stored in temporary storage and auto-delete after 7 days. + Maximum file size is 25MB. + + Unlike `/v1/media/upload` (which uses upload tokens for end-user flows), + this endpoint uses standard Bearer token authentication for programmatic use. + tags: [Messages] + security: [{ bearerAuth: [] }] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file] + properties: + file: + type: string + format: binary + description: The file to upload (max 25MB) + contentType: + type: string + description: 'Override MIME type (e.g. "image/jpeg"). Auto-detected from file if not provided.' + responses: + '200': + description: File uploaded successfully + content: + application/json: + schema: + type: object + properties: + url: { type: string, description: Publicly accessible URL for the uploaded file } + filename: { type: string, description: Generated unique filename } + contentType: { type: string, description: MIME type of the file } + size: { type: integer, description: File size in bytes } + '400': + description: No file provided or file too large + '401': { $ref: '#/components/responses/Unauthorized' } /v1/accounts/{accountId}/messenger-menu: get: diff --git a/pyproject.toml b/pyproject.toml index c24ef42..3ba3e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.31" +version = "1.3.32" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 5b59943..664c9eb 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.31" +__version__ = "1.3.32" __all__ = [ # Client diff --git a/src/late/mcp/generated_tools.py b/src/late/mcp/generated_tools.py index 38bc1c6..d2d066a 100644 --- a/src/late/mcp/generated_tools.py +++ b/src/late/mcp/generated_tools.py @@ -2807,6 +2807,97 @@ def messages_edit_inbox_message( except Exception as e: return f"Error: {e}" + @mcp.tool() + def messages_delete_inbox_message( + conversation_id: str, message_id: str, account_id: str + ) -> str: + """Delete message + + Args: + conversation_id: The conversation ID (required) + message_id: The platform message ID to delete (required) + account_id: Social account ID (required)""" + client = _get_client() + try: + response = client.messages.delete_inbox_message( + conversation_id=conversation_id, + message_id=message_id, + account_id=account_id, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def messages_send_typing_indicator(conversation_id: str, account_id: str) -> str: + """Send typing indicator + + Args: + conversation_id: The conversation ID (required) + account_id: Social account ID (required)""" + client = _get_client() + try: + response = client.messages.send_typing_indicator( + conversation_id=conversation_id, account_id=account_id + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def messages_add_message_reaction( + conversation_id: str, message_id: str, account_id: str, emoji: str + ) -> str: + """Add reaction + + Args: + conversation_id: The conversation ID (required) + message_id: The platform message ID to react to (required) + account_id: Social account ID (required) + emoji: Emoji character (e.g. "👍", "❤️") (required)""" + client = _get_client() + try: + response = client.messages.add_message_reaction( + conversation_id=conversation_id, + message_id=message_id, + account_id=account_id, + emoji=emoji, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def messages_remove_message_reaction( + conversation_id: str, message_id: str, account_id: str + ) -> str: + """Remove reaction + + Args: + conversation_id: The conversation ID (required) + message_id: The platform message ID (required) + account_id: Social account ID (required)""" + client = _get_client() + try: + response = client.messages.remove_message_reaction( + conversation_id=conversation_id, + message_id=message_id, + account_id=account_id, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def messages_upload_media_direct() -> str: + """Upload media file""" + client = _get_client() + try: + response = client.messages.upload_media_direct() + return _format_response(response) + except Exception as e: + return f"Error: {e}" + # POSTS @mcp.tool() diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 85d34e8..66af307 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-29T13:17:02+00:00 +# timestamp: 2026-03-29T16:52:53+00:00 from __future__ import annotations diff --git a/src/late/resources/_generated/messages.py b/src/late/resources/_generated/messages.py index 99f4cf2..ee5667c 100644 --- a/src/late/resources/_generated/messages.py +++ b/src/late/resources/_generated/messages.py @@ -159,6 +159,58 @@ def edit_inbox_message( data=payload, ) + def delete_inbox_message( + self, conversation_id: str, message_id: str, account_id: str + ) -> dict[str, Any]: + """Delete message""" + params = self._build_params( + account_id=account_id, + ) + return self._client._delete( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", + params=params, + ) + + def send_typing_indicator( + self, conversation_id: str, account_id: str + ) -> dict[str, Any]: + """Send typing indicator""" + payload = self._build_payload( + account_id=account_id, + ) + return self._client._post( + f"/v1/inbox/conversations/{conversation_id}/typing", data=payload + ) + + def add_message_reaction( + self, conversation_id: str, message_id: str, account_id: str, emoji: str + ) -> dict[str, Any]: + """Add reaction""" + payload = self._build_payload( + account_id=account_id, + emoji=emoji, + ) + return self._client._post( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", + data=payload, + ) + + def remove_message_reaction( + self, conversation_id: str, message_id: str, account_id: str + ) -> dict[str, Any]: + """Remove reaction""" + params = self._build_params( + account_id=account_id, + ) + return self._client._delete( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", + params=params, + ) + + def upload_media_direct(self) -> dict[str, Any]: + """Upload media file""" + return self._client._post("/v1/media/upload-direct") + async def alist_inbox_conversations( self, *, @@ -269,3 +321,55 @@ async def aedit_inbox_message( f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", data=payload, ) + + async def adelete_inbox_message( + self, conversation_id: str, message_id: str, account_id: str + ) -> dict[str, Any]: + """Delete message (async)""" + params = self._build_params( + account_id=account_id, + ) + return await self._client._adelete( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", + params=params, + ) + + async def asend_typing_indicator( + self, conversation_id: str, account_id: str + ) -> dict[str, Any]: + """Send typing indicator (async)""" + payload = self._build_payload( + account_id=account_id, + ) + return await self._client._apost( + f"/v1/inbox/conversations/{conversation_id}/typing", data=payload + ) + + async def aadd_message_reaction( + self, conversation_id: str, message_id: str, account_id: str, emoji: str + ) -> dict[str, Any]: + """Add reaction (async)""" + payload = self._build_payload( + account_id=account_id, + emoji=emoji, + ) + return await self._client._apost( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", + data=payload, + ) + + async def aremove_message_reaction( + self, conversation_id: str, message_id: str, account_id: str + ) -> dict[str, Any]: + """Remove reaction (async)""" + params = self._build_params( + account_id=account_id, + ) + return await self._client._adelete( + f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", + params=params, + ) + + async def aupload_media_direct(self) -> dict[str, Any]: + """Upload media file (async)""" + return await self._client._apost("/v1/media/upload-direct") diff --git a/uv.lock b/uv.lock index 27524f7..ac78eea 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.30" +version = "1.3.31" source = { editable = "." } dependencies = [ { name = "httpx" }, From 9953d8c7e41f6a7bfce39b92b3b18dccc81a908e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Mar 2026 17:30:47 +0000 Subject: [PATCH 07/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.33 --- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/models/_generated/models.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ba3e55..05d8ab5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.32" +version = "1.3.33" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 664c9eb..87439f1 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.32" +__version__ = "1.3.33" __all__ = [ # Client diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 66af307..0fef81f 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-29T16:52:53+00:00 +# timestamp: 2026-03-29T17:30:38+00:00 from __future__ import annotations diff --git a/uv.lock b/uv.lock index ac78eea..1d3152e 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.31" +version = "1.3.32" source = { editable = "." } dependencies = [ { name = "httpx" }, From d172becdf2d731d325f82c8948b398e1a349482b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 30 Mar 2026 11:33:00 +0000 Subject: [PATCH 08/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.34 --- openapi.yaml | 12 ++++++++++-- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/models/_generated/models.py | 14 +++++++++++--- uv.lock | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 53e1d63..7cc893b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1201,7 +1201,7 @@ components: instagramThumbnail: type: string format: uri - description: Optional custom cover image URL for Instagram Reels + description: "Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy)." tiktokProcessed: type: boolean description: Internal flag indicating the image was resized for TikTok @@ -1550,8 +1550,16 @@ components: thumbOffset: type: integer minimum: 0 - description: Millisecond offset from video start for the Reel thumbnail. Ignored if a custom thumbnail URL is provided. Defaults to 0. + description: Millisecond offset from video start for the Reel cover frame. Ignored when instagramThumbnail or reelCover is provided. Defaults to 0. example: 5000 + instagramThumbnail: + type: string + format: uri + description: Custom cover image URL for Instagram Reels (JPG or PNG, publicly accessible). Overrides thumbOffset when provided. Also accepted as reelCover (alias). + reelCover: + type: string + format: uri + description: Alias for instagramThumbnail. If both are provided, instagramThumbnail takes priority. description: Feed aspect ratio 0.8-1.91, carousels up to 10 items, stories require media (no captions). User tag coordinates 0.0-1.0 from top-left. Images over 8 MB and videos over platform limits are auto-compressed. LinkedInPlatformData: diff --git a/pyproject.toml b/pyproject.toml index 05d8ab5..0711800 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.33" +version = "1.3.34" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 87439f1..55839f3 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.33" +__version__ = "1.3.34" __all__ = [ # Client diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 0fef81f..84d749b 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-29T17:30:38+00:00 +# timestamp: 2026-03-30T11:32:51+00:00 from __future__ import annotations @@ -1074,7 +1074,7 @@ class MediaItem(BaseModel): """ instagramThumbnail: AnyUrl | None = None """ - Optional custom cover image URL for Instagram Reels + Custom cover image URL for Instagram Reels. Can also be set via platformSpecificData.instagramThumbnail or platformSpecificData.reelCover. Resolution order: this field > platformSpecificData.instagramThumbnail > platformSpecificData.reelCover > platformSpecificData.thumbnailUrl (legacy). """ tiktokProcessed: bool | None = None """ @@ -1412,7 +1412,15 @@ class InstagramPlatformData(BaseModel): """ thumbOffset: Annotated[int | None, Field(examples=[5000], ge=0)] = None """ - Millisecond offset from video start for the Reel thumbnail. Ignored if a custom thumbnail URL is provided. Defaults to 0. + Millisecond offset from video start for the Reel cover frame. Ignored when instagramThumbnail or reelCover is provided. Defaults to 0. + """ + instagramThumbnail: AnyUrl | None = None + """ + Custom cover image URL for Instagram Reels (JPG or PNG, publicly accessible). Overrides thumbOffset when provided. Also accepted as reelCover (alias). + """ + reelCover: AnyUrl | None = None + """ + Alias for instagramThumbnail. If both are provided, instagramThumbnail takes priority. """ diff --git a/uv.lock b/uv.lock index 1d3152e..98745c9 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.32" +version = "1.3.33" source = { editable = "." } dependencies = [ { name = "httpx" }, From fb66ab1b8853bd0de6518820f63df364cb1d365a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 30 Mar 2026 16:57:59 +0000 Subject: [PATCH 09/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.35 --- README.md | 2 + openapi.yaml | 92 ++++++++++++++++++++++++ pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/mcp/generated_tools.py | 34 +++++++++ src/late/models/_generated/models.py | 6 +- src/late/resources/_generated/connect.py | 40 +++++++++++ uv.lock | 2 +- 8 files changed, 176 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 98569ee..ce6ae27 100644 --- a/README.md +++ b/README.md @@ -323,11 +323,13 @@ Both `from zernio import ...` and `from late import ...` work identically. The ` | `connect.get_reddit_flairs()` | List subreddit flairs | | `connect.get_reddit_subreddits()` | List Reddit subreddits | | `connect.get_telegram_connect_status()` | Generate Telegram code | +| `connect.get_youtube_playlists()` | List YouTube playlists | | `connect.update_facebook_page()` | Update Facebook page | | `connect.update_gmb_location()` | Update GBP location | | `connect.update_linked_in_organization()` | Switch LinkedIn account type | | `connect.update_pinterest_boards()` | Set default Pinterest board | | `connect.update_reddit_subreddits()` | Set default subreddit | +| `connect.update_youtube_default_playlist()` | Set default YouTube playlist | | `connect.complete_telegram_connect()` | Check Telegram status | | `connect.connect_bluesky_credentials()` | Connect Bluesky account | | `connect.connect_whats_app_credentials()` | Connect WhatsApp via credentials | diff --git a/openapi.yaml b/openapi.yaml index 7cc893b..8e2c305 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1629,6 +1629,9 @@ components: type: string default: '22' description: "YouTube video category ID. Defaults to 22 (People & Blogs). Common: 1 (Film), 2 (Autos), 10 (Music), 15 (Pets), 17 (Sports), 20 (Gaming), 23 (Comedy), 24 (Entertainment), 25 (News), 26 (Howto), 27 (Education), 28 (Science & Tech)." + playlistId: + type: string + description: "Optional YouTube playlist ID to add the video to after upload (e.g. 'PLxxxxxxxxxxxxx'). Use GET /v1/accounts/{id}/youtube-playlists to list available playlists. Works for both immediate and scheduled uploads. Quota cost: 50 YouTube API units per call." description: Videos under 3 min auto-detected as Shorts. Custom thumbnails for regular videos only. Scheduled videos are uploaded immediately with the specified visibility. GoogleBusinessPlatformData: @@ -9897,6 +9900,95 @@ paths: '401': { $ref: '#/components/responses/Unauthorized' } '404': { description: Account not found } + /v1/accounts/{accountId}/youtube-playlists: + get: + operationId: getYoutubePlaylists + tags: [Connect] + summary: List YouTube playlists + description: Returns the playlists available for a connected YouTube account. Use this to get a playlist ID when creating a YouTube post with the `playlistId` field. + parameters: + - name: accountId + in: path + required: true + schema: { type: string } + responses: + '200': + description: Playlists list + content: + application/json: + schema: + type: object + properties: + playlists: + type: array + items: + type: object + properties: + id: { type: string } + title: { type: string } + description: { type: string } + privacy: { type: string, enum: [public, private, unlisted] } + itemCount: { type: integer } + thumbnailUrl: { type: string } + defaultPlaylistId: + type: string + nullable: true + example: + playlists: + - id: "PLxxxxxxxxxxxxx" + title: "Tutorials" + description: "Step-by-step video tutorials" + privacy: "public" + itemCount: 24 + thumbnailUrl: "https://i.ytimg.com/vi/xxx/mqdefault.jpg" + - id: "PLyyyyyyyyyyyyy" + title: "Vlogs" + description: "Weekly vlogs" + privacy: "public" + itemCount: 52 + thumbnailUrl: "https://i.ytimg.com/vi/yyy/mqdefault.jpg" + defaultPlaylistId: null + '400': { description: Not a YouTube account } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { description: Account not found } + put: + operationId: updateYoutubeDefaultPlaylist + tags: [Connect] + summary: Set default YouTube playlist + description: Sets the default playlist used when publishing videos for this account. When a post does not specify a `playlistId`, the default playlist is not automatically used (it is stored for client-side convenience). + parameters: + - name: accountId + in: path + required: true + schema: { type: string } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [defaultPlaylistId] + properties: + defaultPlaylistId: { type: string } + defaultPlaylistName: { type: string } + example: + defaultPlaylistId: "PLxxxxxxxxxxxxx" + defaultPlaylistName: "Tutorials" + responses: + '200': + description: Default playlist set + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + example: + success: true + '400': { description: Invalid request } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { description: Account not found } + /v1/accounts/{accountId}/gmb-locations: get: operationId: getGmbLocations diff --git a/pyproject.toml b/pyproject.toml index 0711800..66ab77a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.34" +version = "1.3.35" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 55839f3..15adfa1 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.34" +__version__ = "1.3.35" __all__ = [ # Client diff --git a/src/late/mcp/generated_tools.py b/src/late/mcp/generated_tools.py index d2d066a..c0abd9f 100644 --- a/src/late/mcp/generated_tools.py +++ b/src/late/mcp/generated_tools.py @@ -2150,6 +2150,40 @@ def connect_update_pinterest_boards( except Exception as e: return f"Error: {e}" + @mcp.tool() + def connect_get_youtube_playlists(account_id: str) -> str: + """List YouTube playlists + + Args: + account_id: (required)""" + client = _get_client() + try: + response = client.connect.get_youtube_playlists(account_id=account_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def connect_update_youtube_default_playlist( + account_id: str, default_playlist_id: str, default_playlist_name: str = "" + ) -> str: + """Set default YouTube playlist + + Args: + account_id: (required) + default_playlist_id: (required) + default_playlist_name""" + client = _get_client() + try: + response = client.connect.update_youtube_default_playlist( + account_id=account_id, + default_playlist_id=default_playlist_id, + default_playlist_name=default_playlist_name, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + @mcp.tool() def connect_get_gmb_locations(account_id: str) -> str: """List GBP locations diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 84d749b..aafea82 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-30T11:32:51+00:00 +# timestamp: 2026-03-30T16:57:51+00:00 from __future__ import annotations @@ -1509,6 +1509,10 @@ class YouTubePlatformData(BaseModel): """ YouTube video category ID. Defaults to 22 (People & Blogs). Common: 1 (Film), 2 (Autos), 10 (Music), 15 (Pets), 17 (Sports), 20 (Gaming), 23 (Comedy), 24 (Entertainment), 25 (News), 26 (Howto), 27 (Education), 28 (Science & Tech). """ + playlistId: str | None = None + """ + Optional YouTube playlist ID to add the video to after upload (e.g. 'PLxxxxxxxxxxxxx'). Use GET /v1/accounts/{id}/youtube-playlists to list available playlists. Works for both immediate and scheduled uploads. Quota cost: 50 YouTube API units per call. + """ class Type1(Enum): diff --git a/src/late/resources/_generated/connect.py b/src/late/resources/_generated/connect.py index 1e8b8fc..75c3407 100644 --- a/src/late/resources/_generated/connect.py +++ b/src/late/resources/_generated/connect.py @@ -350,6 +350,26 @@ def update_pinterest_boards( f"/v1/accounts/{account_id}/pinterest-boards", data=payload ) + def get_youtube_playlists(self, account_id: str) -> dict[str, Any]: + """List YouTube playlists""" + return self._client._get(f"/v1/accounts/{account_id}/youtube-playlists") + + def update_youtube_default_playlist( + self, + account_id: str, + default_playlist_id: str, + *, + default_playlist_name: str | None = None, + ) -> dict[str, Any]: + """Set default YouTube playlist""" + payload = self._build_payload( + default_playlist_id=default_playlist_id, + default_playlist_name=default_playlist_name, + ) + return self._client._put( + f"/v1/accounts/{account_id}/youtube-playlists", data=payload + ) + def get_gmb_locations(self, account_id: str) -> dict[str, Any]: """List GBP locations""" return self._client._get(f"/v1/accounts/{account_id}/gmb-locations") @@ -715,6 +735,26 @@ async def aupdate_pinterest_boards( f"/v1/accounts/{account_id}/pinterest-boards", data=payload ) + async def aget_youtube_playlists(self, account_id: str) -> dict[str, Any]: + """List YouTube playlists (async)""" + return await self._client._aget(f"/v1/accounts/{account_id}/youtube-playlists") + + async def aupdate_youtube_default_playlist( + self, + account_id: str, + default_playlist_id: str, + *, + default_playlist_name: str | None = None, + ) -> dict[str, Any]: + """Set default YouTube playlist (async)""" + payload = self._build_payload( + default_playlist_id=default_playlist_id, + default_playlist_name=default_playlist_name, + ) + return await self._client._aput( + f"/v1/accounts/{account_id}/youtube-playlists", data=payload + ) + async def aget_gmb_locations(self, account_id: str) -> dict[str, Any]: """List GBP locations (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/gmb-locations") diff --git a/uv.lock b/uv.lock index 98745c9..05d7655 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.33" +version = "1.3.34" source = { editable = "." } dependencies = [ { name = "httpx" }, From fc30770538eff8b9970ac96656575c71b271e895 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:53:30 +0200 Subject: [PATCH 10/17] fix: add WhatsApp template models to __init__.py exports --- src/late/models/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/late/models/__init__.py b/src/late/models/__init__.py index 56bc4de..68d3c7f 100644 --- a/src/late/models/__init__.py +++ b/src/late/models/__init__.py @@ -73,6 +73,13 @@ UserGetResponse, UsersListResponse, Visibility, + # WhatsApp template models + WhatsAppBodyComponent, + WhatsAppButtonsComponent, + WhatsAppFooterComponent, + WhatsAppHeaderComponent, + WhatsAppTemplateButton, + WhatsAppTemplateComponent, YouTubePlatformData, ) @@ -102,6 +109,13 @@ "LinkedInPlatformData", "YouTubePlatformData", "PinterestPlatformData", + # WhatsApp template models + "WhatsAppBodyComponent", + "WhatsAppButtonsComponent", + "WhatsAppFooterComponent", + "WhatsAppHeaderComponent", + "WhatsAppTemplateButton", + "WhatsAppTemplateComponent", # Base responses "Pagination", "ErrorResponse", From 00a665019e4a09e891dff92371bfc5e0d86be315 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:55:11 +0200 Subject: [PATCH 11/17] fix: remove comments from import block that break validation regex --- src/late/models/__init__.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/late/models/__init__.py b/src/late/models/__init__.py index 68d3c7f..63c2468 100644 --- a/src/late/models/__init__.py +++ b/src/late/models/__init__.py @@ -11,12 +11,10 @@ # Import specific commonly used models for convenience from ._generated.models import ( AccountGetResponse, - # Accounts responses AccountsListResponse, AccountWithFollowerStats, CaptionResponse, DownloadFormat, - # Tools responses DownloadResponse, ErrorResponse, FacebookPlatformData, @@ -26,26 +24,21 @@ InstagramPlatformData, LinkedInPlatformData, MediaItem, - # Media responses MediaUploadResponse, - # Base responses Pagination, PinterestPlatformData, PlatformTarget, - # Core models Post, PostCreateResponse, PostDeleteResponse, PostGetResponse, PostRetryResponse, - # Posts responses PostsListResponse, PostUpdateResponse, Profile, ProfileCreateResponse, ProfileDeleteResponse, ProfileGetResponse, - # Profiles responses ProfilesListResponse, ProfileUpdateResponse, QueueDeleteResponse, @@ -53,13 +46,10 @@ QueuePreviewResponse, QueueSchedule, QueueSlot, - # Queue responses QueueSlotsResponse, QueueUpdateResponse, SocialAccount, - # Enums Status, - # Platform-specific TikTokPlatformData, TranscriptResponse, TranscriptSegment, @@ -68,12 +58,10 @@ UploadedFile, UploadTokenResponse, UploadTokenStatusResponse, - # Users responses User, UserGetResponse, UsersListResponse, Visibility, - # WhatsApp template models WhatsAppBodyComponent, WhatsAppButtonsComponent, WhatsAppFooterComponent, @@ -89,7 +77,6 @@ ) __all__ = [ - # Core models "Post", "MediaItem", "PlatformTarget", @@ -97,11 +84,9 @@ "SocialAccount", "QueueSlot", "QueueSchedule", - # Enums "Status", "Type", "Visibility", - # Platform-specific "TikTokPlatformData", "TwitterPlatformData", "InstagramPlatformData", @@ -109,47 +94,39 @@ "LinkedInPlatformData", "YouTubePlatformData", "PinterestPlatformData", - # WhatsApp template models "WhatsAppBodyComponent", "WhatsAppButtonsComponent", "WhatsAppFooterComponent", "WhatsAppHeaderComponent", "WhatsAppTemplateButton", "WhatsAppTemplateComponent", - # Base responses "Pagination", "ErrorResponse", - # Posts responses "PostsListResponse", "PostGetResponse", "PostCreateResponse", "PostUpdateResponse", "PostDeleteResponse", "PostRetryResponse", - # Profiles responses "ProfilesListResponse", "ProfileGetResponse", "ProfileCreateResponse", "ProfileUpdateResponse", "ProfileDeleteResponse", - # Accounts responses "AccountsListResponse", "AccountGetResponse", "FollowerStatsResponse", "AccountWithFollowerStats", - # Media responses "MediaUploadResponse", "MediaLargeUploadResponse", "UploadedFile", "UploadTokenResponse", "UploadTokenStatusResponse", - # Queue responses "QueueSlotsResponse", "QueueUpdateResponse", "QueueDeleteResponse", "QueuePreviewResponse", "QueueNextSlotResponse", - # Tools responses "DownloadResponse", "DownloadFormat", "TranscriptResponse", @@ -157,7 +134,6 @@ "HashtagCheckResponse", "HashtagInfo", "CaptionResponse", - # Users responses "User", "UsersListResponse", "UserGetResponse", From 509e9b72ba0dc706fac761933a9a7fd5d0825813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=92scar=20P=C3=A9rez?= <60936394+Ooscaar@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:16:37 +0200 Subject: [PATCH 12/17] fix: regenerate SDK and fix broken model imports (#19) * fix: fix broken model imports blocking regenerate pipeline Move 7 models removed from OpenAPI spec (CaptionResponse, DownloadResponse, DownloadFormat, HashtagCheckResponse, HashtagInfo, TranscriptResponse, TranscriptSegment) to responses.py since the endpoints still work. Update models/__init__.py and tools.py imports accordingly. Fix test_exhaustive Type enum references (Type -> Type5). Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use string values in media type tests instead of unstable Type5 enum The generated enum name (Type5) is not stable across regenerations. Use string values with Pydantic coercion instead. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/late/models/__init__.py | 14 ++++----- src/late/models/responses.py | 58 +++++++++++++++++++++++++++++++++++- src/late/resources/tools.py | 2 +- tests/test_exhaustive.py | 16 +++++----- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/late/models/__init__.py b/src/late/models/__init__.py index 63c2468..d6c5ea7 100644 --- a/src/late/models/__init__.py +++ b/src/late/models/__init__.py @@ -13,14 +13,9 @@ AccountGetResponse, AccountsListResponse, AccountWithFollowerStats, - CaptionResponse, - DownloadFormat, - DownloadResponse, ErrorResponse, FacebookPlatformData, FollowerStatsResponse, - HashtagCheckResponse, - HashtagInfo, InstagramPlatformData, LinkedInPlatformData, MediaItem, @@ -51,8 +46,6 @@ SocialAccount, Status, TikTokPlatformData, - TranscriptResponse, - TranscriptSegment, TwitterPlatformData, Type, UploadedFile, @@ -73,7 +66,14 @@ # SDK-specific models (not from OpenAPI) from .responses import ( + CaptionResponse, + DownloadFormat, + DownloadResponse, + HashtagCheckResponse, + HashtagInfo, MediaLargeUploadResponse, + TranscriptResponse, + TranscriptSegment, ) __all__ = [ diff --git a/src/late/models/responses.py b/src/late/models/responses.py index f1ef4e2..ba36442 100644 --- a/src/late/models/responses.py +++ b/src/late/models/responses.py @@ -7,7 +7,63 @@ from __future__ import annotations -from pydantic import BaseModel +from enum import Enum + +from pydantic import AnyUrl, BaseModel + +# --------------------------------------------------------------------------- +# Tools endpoint models (endpoints removed from OpenAPI spec but still +# functional for existing customers) +# --------------------------------------------------------------------------- + + +class DownloadFormat(BaseModel): + formatId: str | None = None + ext: str | None = None + resolution: str | None = None + filesize: int | None = None + quality: str | None = None + + +class DownloadResponse(BaseModel): + url: AnyUrl | None = None + title: str | None = None + thumbnail: AnyUrl | None = None + duration: int | None = None + formats: list[DownloadFormat] | None = None + + +class TranscriptSegment(BaseModel): + text: str | None = None + start: float | None = None + duration: float | None = None + + +class TranscriptResponse(BaseModel): + transcript: str | None = None + segments: list[TranscriptSegment] | None = None + language: str | None = None + + +class HashtagStatus(str, Enum): + SAFE = "safe" + BANNED = "banned" + RESTRICTED = "restricted" + UNKNOWN = "unknown" + + +class HashtagInfo(BaseModel): + hashtag: str | None = None + status: HashtagStatus | None = None + postCount: int | None = None + + +class HashtagCheckResponse(BaseModel): + hashtags: list[HashtagInfo] | None = None + + +class CaptionResponse(BaseModel): + caption: str | None = None class MediaLargeUploadResponse(BaseModel): diff --git a/src/late/resources/tools.py b/src/late/resources/tools.py index cdf1376..2019e9e 100644 --- a/src/late/resources/tools.py +++ b/src/late/resources/tools.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING -from late.models import ( +from late.models.responses import ( CaptionResponse, DownloadResponse, HashtagCheckResponse, diff --git a/tests/test_exhaustive.py b/tests/test_exhaustive.py index d283c6e..bdf1a48 100644 --- a/tests/test_exhaustive.py +++ b/tests/test_exhaustive.py @@ -271,13 +271,12 @@ def test_status_enum_values(self): assert Status.FAILED.value == "failed" def test_type_enum_values(self): - """Test Type enum has expected values.""" - from late.models import Type + """Test MediaItem type field accepts media types.""" + from late.models._generated.models import MediaItem - assert hasattr(Type, "IMAGE") - assert hasattr(Type, "VIDEO") - assert Type.IMAGE.value == "image" - assert Type.VIDEO.value == "video" + MediaType = MediaItem.model_fields["type"].annotation + assert MediaItem(type="image").type.value == "image" + assert MediaItem(type="video").type.value == "video" def test_tiktok_settings_creation(self): """Test TikTokPlatformData model creation.""" @@ -294,11 +293,10 @@ def test_tiktok_settings_creation(self): def test_media_item_creation(self): """Test MediaItem with type enum.""" - from late.models import Type from late.models._generated.models import MediaItem - item = MediaItem(type=Type.IMAGE) - assert item.type == Type.IMAGE + item = MediaItem(type="image") + assert item.type.value == "image" # ============================================================================ From ce7d546c569638cd9f8a13022e55d02c8cf64fd0 Mon Sep 17 00:00:00 2001 From: Oscar Perez Date: Thu, 2 Apr 2026 16:28:25 +0200 Subject: [PATCH 13/17] fix ci --- src/late/models/__init__.py | 16 ++-------------- tests/test_exhaustive.py | 1 - uv.lock | 2 +- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/late/models/__init__.py b/src/late/models/__init__.py index d6c5ea7..70f9266 100644 --- a/src/late/models/__init__.py +++ b/src/late/models/__init__.py @@ -55,17 +55,11 @@ UserGetResponse, UsersListResponse, Visibility, - WhatsAppBodyComponent, - WhatsAppButtonsComponent, - WhatsAppFooterComponent, - WhatsAppHeaderComponent, - WhatsAppTemplateButton, - WhatsAppTemplateComponent, YouTubePlatformData, ) -# SDK-specific models (not from OpenAPI) -from .responses import ( +# SDK-specific models (not from OpenAPI) — intentionally override generated stubs +from .responses import ( # type: ignore[assignment] CaptionResponse, DownloadFormat, DownloadResponse, @@ -94,12 +88,6 @@ "LinkedInPlatformData", "YouTubePlatformData", "PinterestPlatformData", - "WhatsAppBodyComponent", - "WhatsAppButtonsComponent", - "WhatsAppFooterComponent", - "WhatsAppHeaderComponent", - "WhatsAppTemplateButton", - "WhatsAppTemplateComponent", "Pagination", "ErrorResponse", "PostsListResponse", diff --git a/tests/test_exhaustive.py b/tests/test_exhaustive.py index bdf1a48..28c68b0 100644 --- a/tests/test_exhaustive.py +++ b/tests/test_exhaustive.py @@ -274,7 +274,6 @@ def test_type_enum_values(self): """Test MediaItem type field accepts media types.""" from late.models._generated.models import MediaItem - MediaType = MediaItem.model_fields["type"].annotation assert MediaItem(type="image").type.value == "image" assert MediaItem(type="video").type.value == "video" diff --git a/uv.lock b/uv.lock index 05d7655..10879a9 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,7 +1905,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.3.34" +version = "1.3.35" source = { editable = "." } dependencies = [ { name = "httpx" }, From 98a4775726481a821e8036726fb51f7f2ef9d66a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Apr 2026 14:37:33 +0000 Subject: [PATCH 14/17] chore: regenerate from OpenAPI spec - Auto-generated SDK updates - Version: 1.3.36 --- README.md | 43 +- openapi.yaml | 1616 +++++++++++------ pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/mcp/generated_tools.py | 672 +++++-- src/late/models/_generated/models.py | 443 ++++- src/late/resources/_generated/__init__.py | 8 +- src/late/resources/_generated/accounts.py | 8 + src/late/resources/_generated/ad_audiences.py | 175 ++ src/late/resources/_generated/ad_campaigns.py | 173 ++ src/late/resources/_generated/ads.py | 397 ++++ src/late/resources/_generated/posts.py | 16 + src/late/resources/_generated/whatsapp.py | 8 +- 13 files changed, 2794 insertions(+), 769 deletions(-) create mode 100644 src/late/resources/_generated/ad_audiences.py create mode 100644 src/late/resources/_generated/ad_campaigns.py create mode 100644 src/late/resources/_generated/ads.py diff --git a/README.md b/README.md index ce6ae27..11bb689 100644 --- a/README.md +++ b/README.md @@ -275,19 +275,6 @@ Both `from zernio import ...` and `from late import ...` work identically. The ` | `media.upload_large_bytes()` | Upload large file from bytes | | `media.upload_multiple()` | Upload multiple files | -### Tools -| Method | Description | -|--------|-------------| -| `tools.get_you_tube_transcript()` | Get YouTube transcript | -| `tools.check_instagram_hashtags()` | Check IG hashtag bans | -| `tools.download_bluesky_media()` | Download Bluesky media | -| `tools.download_facebook_video()` | Download Facebook video | -| `tools.download_instagram_media()` | Download Instagram media | -| `tools.download_linked_in_video()` | Download LinkedIn video | -| `tools.download_tik_tok_video()` | Download TikTok video | -| `tools.download_twitter_media()` | Download Twitter/X media | -| `tools.download_you_tube_video()` | Download YouTube video | - ### Users | Method | Description | |--------|-------------| @@ -360,6 +347,36 @@ Both `from zernio import ...` and `from late import ...` work identically. The ` | `account_settings.set_messenger_menu()` | Set FB persistent menu | | `account_settings.set_telegram_commands()` | Set TG bot commands | +### Ad Audiences +| Method | Description | +|--------|-------------| +| `ad_audiences.list_ad_audiences()` | List custom audiences | +| `ad_audiences.create_ad_audience()` | Create a custom audience (Meta only) | +| `ad_audiences.get_ad_audience()` | Get audience details | +| `ad_audiences.delete_ad_audience()` | Delete a custom audience | +| `ad_audiences.add_users_to_ad_audience()` | Add users to a customer list audience | + +### Ad Campaigns +| Method | Description | +|--------|-------------| +| `ad_campaigns.list_ad_campaigns()` | List campaigns with aggregate metrics | +| `ad_campaigns.get_ad_tree()` | Get nested campaign/ad-set/ad tree | +| `ad_campaigns.update_ad_campaign_status()` | Pause or resume a campaign | + +### Ads +| Method | Description | +|--------|-------------| +| `ads.list_ad_accounts()` | List ad accounts for a social account | +| `ads.list_ads()` | List ads | +| `ads.create_standalone_ad()` | Create a standalone ad with custom creative | +| `ads.get_ad()` | Get ad details | +| `ads.get_ad_analytics()` | Get ad analytics with daily breakdown | +| `ads.update_ad()` | Update ad (pause/resume, budget, targeting, name) | +| `ads.delete_ad()` | Cancel an ad | +| `ads.boost_post()` | Boost an existing post as a paid ad | +| `ads.search_ad_interests()` | Search targeting interests | +| `ads.sync_external_ads()` | Sync external ads from platform ad managers | + ### Broadcasts | Method | Description | |--------|-------------| diff --git a/openapi.yaml b/openapi.yaml index 8e2c305..7ceab20 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -164,7 +164,6 @@ tags: - name: Invites - name: Connect - name: Media - # - name: Video Clips # AI Clipping feature temporarily disabled - name: Reddit Search - name: Facebook - name: GMB Reviews @@ -201,11 +200,6 @@ tags: description: | X/Twitter-specific engagement endpoints for retweeting, bookmarking, and following. Rate limits: 50 requests per 15-min window per user. Retweets share the 300/3hr creation limit with tweet creation. - - name: Tools - description: | - Media download and utility tools. Available to paid plans only. - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - All responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. - name: Validate description: | Pre-flight validation endpoints. Check post content, character limits, media URLs, and subreddit existence before publishing. @@ -233,6 +227,22 @@ tags: Comment-to-DM growth automations. Set up keyword triggers on Instagram/Facebook posts so commenters automatically receive a DM. Supports dedup, optional public comment reply, and auto-creates contacts. + - name: Ads + description: | + Paid advertising management across Meta (Facebook/Instagram), Google, TikTok, LinkedIn, Pinterest, and X/Twitter. + Create, boost, pause/resume ads and campaigns, view metrics, manage audiences, and sync external ads. + Requires the Ads add-on. + - name: Ad Campaigns + description: | + Campaign-level operations. Campaigns are virtual aggregations of ads grouped by their platform campaign ID. + List campaigns with aggregate metrics, or pause/resume all ads in a campaign at once. + Requires the Ads add-on. + - name: Ad Audiences + description: | + Custom audience management for ad targeting. Create customer lists, website retargeting audiences, + and lookalike audiences. Upload user data (hashed server-side). Currently Meta-only for creation, + read-only for other platforms. + Requires the Ads add-on. - name: Webhooks description: | Configure webhooks for real-time notifications. Events: post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, account.connected, account.disconnected, message.received, comment.received, webhook.test. @@ -309,6 +319,135 @@ components: details: type: object additionalProperties: true + WhatsAppTemplateButton: + type: object + required: [type, text] + properties: + type: + type: string + enum: [quick_reply, url, phone_number, otp, flow, mpm, catalog] + text: + type: string + url: + type: string + format: uri + description: Required when type is URL + example: + type: array + items: { type: string } + description: Example values for URL suffix variables + phone_number: + type: string + description: Required when type is phone_number + otp_type: + type: string + enum: [copy_code, one_tap, zero_tap] + description: Required when type is otp + autofill_text: + type: string + package_name: + type: string + signature_hash: + type: string + flow_id: + type: string + flow_name: + type: string + flow_json: + type: string + flow_action: + type: string + navigate_screen: + type: string + WhatsAppTemplateComponent: + oneOf: + - $ref: '#/components/schemas/WhatsAppHeaderComponent' + - $ref: '#/components/schemas/WhatsAppBodyComponent' + - $ref: '#/components/schemas/WhatsAppFooterComponent' + - $ref: '#/components/schemas/WhatsAppButtonsComponent' + discriminator: + propertyName: type + mapping: + header: '#/components/schemas/WhatsAppHeaderComponent' + body: '#/components/schemas/WhatsAppBodyComponent' + footer: '#/components/schemas/WhatsAppFooterComponent' + buttons: '#/components/schemas/WhatsAppButtonsComponent' + WhatsAppHeaderComponent: + type: object + required: [type, format] + properties: + type: + type: string + enum: [header] + format: + type: string + enum: [text, image, video, gif, document, location] + text: + type: string + description: Header text (may include {{1}} variable). Used when format is TEXT. + example: + type: object + properties: + header_text: + type: array + items: { type: string } + description: Sample values for header text variables + header_handle: + type: array + minItems: 1 + maxItems: 1 + items: + type: string + format: uri + description: When the header format is a media type (image, video, gif, document), provide a public URL here. Zernio will download and upload it to WhatsApp on your behalf, replacing it with the internal file handle before creating the template. + WhatsAppBodyComponent: + type: object + required: [type, text] + properties: + type: + type: string + enum: [body] + text: + type: string + description: Body text with optional {{n}} variables + add_security_recommendation: + type: boolean + description: Add security recommendation text (authentication templates only) + example: + type: object + properties: + body_text: + type: array + items: + type: array + items: { type: string } + description: Sample values for body variables (array of arrays) + WhatsAppFooterComponent: + type: object + required: [type] + properties: + type: + type: string + enum: [footer] + text: + type: string + description: Static footer text + code_expiration_minutes: + type: integer + minimum: 1 + description: OTP code expiry in minutes (authentication templates only) + WhatsAppButtonsComponent: + type: object + required: [type, buttons] + properties: + type: + type: string + enum: [buttons] + buttons: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/WhatsAppTemplateButton' FoodMenuLabel: type: object required: [displayName] @@ -1876,6 +2015,17 @@ components: type: string format: date-time description: Last time follower count was updated (only included if user has analytics add-on) + metadata: + type: object + description: | + Platform-specific metadata. Fields vary by platform. For WhatsApp accounts, includes: + - `qualityRating`: Phone number quality rating from Meta (`GREEN`, `YELLOW`, `RED`, or `UNKNOWN`) + - `nameStatus`: Display name review status (`APPROVED`, `PENDING_REVIEW`, `DECLINED`, or `NONE`). Messages cannot be sent until the display name is approved by Meta. + - `messagingLimitTier`: Maximum unique business-initiated conversations per 24h rolling window (`TIER_250`, `TIER_1K`, `TIER_10K`, `TIER_100K`, or `TIER_UNLIMITED`). Scales automatically as quality rating improves. + - `verifiedName`: Meta-verified business display name + - `displayPhoneNumber`: Formatted phone number (e.g., "+1 555-123-4567") + - `wabaId`: WhatsApp Business Account ID + - `phoneNumberId`: Meta phone number ID AccountWithFollowerStats: allOf: - $ref: '#/components/schemas/SocialAccount' @@ -1952,8 +2102,6 @@ components: uploads: { type: integer } profiles: { type: integer } lastReset: { type: string, format: date-time } - # VideoClipJob, VideoClip, VideoClipJobProcessing, VideoClipJobCompleted, VideoClipJobFailed, VideoClipUsageStats - # schemas removed - AI Clipping feature temporarily disabled PostAnalytics: type: object properties: @@ -2375,79 +2523,6 @@ components: format: date-time timezone: type: string - # Tools Responses - DownloadFormat: - type: object - properties: - formatId: - type: string - ext: - type: string - resolution: - type: string - filesize: - type: integer - quality: - type: string - DownloadResponse: - type: object - properties: - url: - type: string - format: uri - title: - type: string - thumbnail: - type: string - format: uri - duration: - type: integer - formats: - type: array - items: - $ref: '#/components/schemas/DownloadFormat' - TranscriptSegment: - type: object - properties: - text: - type: string - start: - type: number - duration: - type: number - TranscriptResponse: - type: object - properties: - transcript: - type: string - segments: - type: array - items: - $ref: '#/components/schemas/TranscriptSegment' - language: - type: string - HashtagInfo: - type: object - properties: - hashtag: - type: string - status: - type: string - enum: [safe, banned, restricted, unknown] - postCount: - type: integer - HashtagCheckResponse: - type: object - properties: - hashtags: - type: array - items: - $ref: '#/components/schemas/HashtagInfo' - CaptionResponse: - type: object - properties: - caption: - type: string # Users Responses User: type: object @@ -2475,6 +2550,134 @@ components: properties: user: $ref: '#/components/schemas/User' + AdMetrics: + type: object + properties: + spend: { type: number } + impressions: { type: integer } + reach: { type: integer } + clicks: { type: integer } + ctr: { type: number, description: Click-through rate (%) } + cpc: { type: number, description: Cost per click } + cpm: { type: number, description: Cost per 1000 impressions } + engagement: { type: integer } + lastSyncedAt: { type: string, format: date-time, description: "Present on individual ads only, not on campaign aggregations" } + Ad: + type: object + properties: + _id: { type: string } + name: { type: string } + platform: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } + status: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error] } + adType: { type: string, enum: [boost, standalone] } + goal: { type: string, enum: [engagement, traffic, awareness, video_views] } + isExternal: { type: boolean, description: True for ads synced from platform ad managers } + budget: + type: object + nullable: true + properties: + amount: { type: number } + type: { type: string, enum: [daily, lifetime] } + metrics: + allOf: + - { $ref: '#/components/schemas/AdMetrics' } + nullable: true + platformAdId: { type: string } + platformAdAccountId: { type: string } + platformCampaignId: { type: string } + platformAdSetId: { type: string } + campaignName: { type: string } + adSetName: { type: string } + creative: + type: object + nullable: true + description: Platform-specific creative data. Fields vary by platform. + properties: + thumbnailUrl: { type: string, description: Primary thumbnail/image URL } + imageUrl: { type: string, description: Alternative image URL } + mediaUrls: + type: array + items: { type: string } + description: All media URLs for this ad (carousel images, multiple assets). Populated for Meta (carousel child_attachments), Google Ads (responsive display marketing_images), and LinkedIn (multi-image posts). + body: { type: string, description: Ad copy/text } + googleHeadline: { type: string, description: Google Ads headline } + googleDescription: { type: string, description: Google Ads description } + linkUrl: { type: string, description: Destination URL } + pinterestImageUrl: { type: string } + pinterestTitle: { type: string } + pinterestDescription: { type: string } + targeting: { type: object } + schedule: + type: object + nullable: true + properties: + startDate: { type: string, format: date-time } + endDate: { type: string, format: date-time } + rejectionReason: { type: string } + createdAt: { type: string, format: date-time } + updatedAt: { type: string, format: date-time } + AdTreeAdSet: + type: object + description: Ad set (or ad group/line item depending on platform) with rolled-up metrics and child ads + properties: + platformAdSetId: { type: string } + adSetName: { type: string } + status: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error], description: Derived from child ad statuses } + adCount: { type: integer } + budget: + type: object + nullable: true + properties: + amount: { type: number } + type: { type: string, enum: [daily, lifetime] } + metrics: { $ref: '#/components/schemas/AdMetrics' } + ads: + type: array + items: { $ref: '#/components/schemas/Ad' } + description: Individual ads within this ad set (capped at 100). Returns a subset of Ad fields from the aggregation (core fields like _id, name, platform, status, budget, metrics, creative, goal are included; targeting and schedule may be absent). + AdTreeCampaign: + type: object + description: Campaign with nested ad sets and rolled-up metrics + properties: + platformCampaignId: { type: string } + platform: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } + campaignName: { type: string } + status: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error], description: Derived from child ad statuses } + adCount: { type: integer, description: Total ads across all ad sets } + adSetCount: { type: integer } + budget: + type: object + nullable: true + properties: + amount: { type: number } + type: { type: string, enum: [daily, lifetime] } + metrics: { $ref: '#/components/schemas/AdMetrics' } + platformAdAccountId: { type: string } + accountId: { type: string } + profileId: { type: string } + adSets: + type: array + items: { $ref: '#/components/schemas/AdTreeAdSet' } + AdCampaign: + type: object + properties: + platformCampaignId: { type: string } + platform: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } + campaignName: { type: string } + status: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error], description: Derived from child ad statuses } + adCount: { type: integer } + budget: + type: object + nullable: true + properties: + amount: { type: number } + type: { type: string, enum: [daily, lifetime] } + metrics: { $ref: '#/components/schemas/AdMetrics' } + platformAdAccountId: { type: string } + accountId: { type: string } + profileId: { type: string } + earliestAd: { type: string, format: date-time } + latestAd: { type: string, format: date-time } webhooks: post.scheduled: post: @@ -2644,473 +2847,71 @@ webhooks: security: - bearerAuth: [] paths: + # NOTE: Tools download endpoints (/v1/tools/{platform}/download, /transcript, /hashtag-checker) removed from docs but still functional for existing customers + # ============================================ - # Tools API - Media Download & Utilities + # Validate # ============================================ - /v1/tools/youtube/download: - get: - operationId: downloadYouTubeVideo - tags: [Tools] - summary: Download YouTube video + /v1/tools/validate/post-length: + post: + operationId: validatePostLength + tags: [Validate] + summary: Validate post character count description: | - Download YouTube videos or audio. Returns available formats or direct download URL. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). + Check weighted character count per platform and whether the text is within each platform's limit. + + Twitter/X uses weighted counting (URLs = 23 chars via t.co, emojis = 2 chars). All other platforms use plain character length. + + Returns counts and limits for all 15 supported platform variants. security: - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: YouTube video URL or video ID - schema: - type: string - example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" - - name: action - in: query - description: "Action to perform: 'download' returns download URL, 'formats' lists available formats" - schema: - type: string - enum: [download, formats] - default: download - - name: format - in: query - description: Desired format (when action=download) - schema: - type: string - enum: [video, audio] - default: video - - name: quality - in: query - description: Desired quality (when action=download) - schema: - type: string - enum: [hd, sd] - default: hd - - name: formatId - in: query - description: Specific format ID from formats list - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [text] + properties: + text: + type: string + description: The post text to check + example: "Check out https://zernio.com for scheduling posts!" responses: "200": - description: Success - headers: - X-RateLimit-Limit: - schema: { type: string } - description: Daily rate limit - X-RateLimit-Remaining: - schema: { type: string } - description: Remaining calls today - X-RateLimit-Reset: - schema: { type: string } - description: Unix timestamp when limit resets + description: Character counts per platform content: application/json: schema: type: object properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - formats: - type: array - items: + text: { type: string } + platforms: + type: object + additionalProperties: type: object properties: - id: { type: string } - label: { type: string } - ext: { type: string } - type: { type: string } - height: { type: integer } - width: { type: integer } - "401": - $ref: '#/components/responses/Unauthorized' - "403": - description: Tools API not available on free plan - "429": - description: Daily rate limit exceeded + count: { type: integer, description: "Character count for this platform" } + limit: { type: integer, description: "Maximum allowed characters" } + valid: { type: boolean, description: "Whether the text is within the limit" } + example: + twitter: { count: 51, limit: 280, valid: true } + twitterPremium: { count: 51, limit: 25000, valid: true } + instagram: { count: 51, limit: 2200, valid: true } + bluesky: { count: 51, limit: 300, valid: true } + snapchat: { count: 51, limit: 160, valid: true } - /v1/tools/youtube/transcript: - get: - operationId: getYouTubeTranscript - tags: [Tools] - summary: Get YouTube transcript + /v1/tools/validate/post: + post: + operationId: validatePost + tags: [Validate] + summary: Validate post content description: | - Extract transcript/captions from a YouTube video. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: YouTube video URL or video ID - schema: - type: string - - name: lang - in: query - description: Language code for transcript - schema: - type: string - default: en - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - videoId: { type: string } - language: { type: string } - fullText: { type: string } - segments: - type: array - items: - type: object - properties: - text: { type: string } - start: { type: number } - duration: { type: number } - "404": - description: No transcript available - "503": - description: Transcript service temporarily unavailable - - /v1/tools/instagram/download: - get: - operationId: downloadInstagramMedia - tags: [Tools] - summary: Download Instagram media - description: | - Download Instagram reels, posts, or photos. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: Instagram reel or post URL - schema: - type: string - example: "https://www.instagram.com/reel/ABC123/" - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - - /v1/tools/instagram/hashtag-checker: - post: - operationId: checkInstagramHashtags - tags: [Tools] - summary: Check IG hashtag bans - description: | - Check if Instagram hashtags are banned, restricted, or safe to use. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [hashtags] - properties: - hashtags: - type: array - maxItems: 20 - items: - type: string - example: ["travel", "followforfollow", "fitness"] - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - results: - type: array - items: - type: object - properties: - hashtag: { type: string } - status: - type: string - enum: [banned, restricted, safe, unknown] - reason: { type: string } - confidence: { type: number } - summary: - type: object - properties: - banned: { type: integer } - restricted: { type: integer } - safe: { type: integer } - - /v1/tools/tiktok/download: - get: - operationId: downloadTikTokVideo - tags: [Tools] - summary: Download TikTok video - description: | - Download TikTok videos with or without watermark. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: TikTok video URL or ID - schema: - type: string - - name: action - in: query - description: "'formats' to list available formats" - schema: - type: string - enum: [download, formats] - default: download - - name: formatId - in: query - description: Specific format ID (0 = no watermark, etc.) - schema: - type: string - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - formats: - type: array - items: - type: object - properties: - id: { type: string } - label: { type: string } - ext: { type: string } - - /v1/tools/twitter/download: - get: - operationId: downloadTwitterMedia - tags: [Tools] - summary: Download Twitter/X media - description: | - Download videos from Twitter/X posts. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: Twitter/X post URL - schema: - type: string - example: "https://x.com/user/status/123456789" - - name: action - in: query - schema: - type: string - enum: [download, formats] - default: download - - name: formatId - in: query - schema: - type: string - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - - /v1/tools/facebook/download: - get: - operationId: downloadFacebookVideo - tags: [Tools] - summary: Download Facebook video - description: | - Download videos and reels from Facebook. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: Facebook video or reel URL - schema: - type: string - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - thumbnail: { type: string, format: uri } - - /v1/tools/linkedin/download: - get: - operationId: downloadLinkedInVideo - tags: [Tools] - summary: Download LinkedIn video - description: | - Download videos from LinkedIn posts. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: LinkedIn post URL - schema: - type: string - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - downloadUrl: { type: string, format: uri } - - /v1/tools/bluesky/download: - get: - operationId: downloadBlueskyMedia - tags: [Tools] - summary: Download Bluesky media - description: | - Download videos from Bluesky posts. - - Rate limits: Build (50/day), Accelerate (500/day), Unlimited (unlimited). - security: - - bearerAuth: [] - parameters: - - name: url - in: query - required: true - description: Bluesky post URL - schema: - type: string - example: "https://bsky.app/profile/user.bsky.social/post/abc123" - responses: - "200": - description: Success - content: - application/json: - schema: - type: object - properties: - success: { type: boolean } - title: { type: string } - text: { type: string } - downloadUrl: { type: string, format: uri } - thumbnail: { type: string, format: uri } - - # ============================================ - # Validate - # ============================================ - /v1/tools/validate/post-length: - post: - operationId: validatePostLength - tags: [Validate] - summary: Validate post character count - description: | - Check weighted character count per platform and whether the text is within each platform's limit. - - Twitter/X uses weighted counting (URLs = 23 chars via t.co, emojis = 2 chars). All other platforms use plain character length. - - Returns counts and limits for all 15 supported platform variants. - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: [text] - properties: - text: - type: string - description: The post text to check - example: "Check out https://zernio.com for scheduling posts!" - responses: - "200": - description: Character counts per platform - content: - application/json: - schema: - type: object - properties: - text: { type: string } - platforms: - type: object - additionalProperties: - type: object - properties: - count: { type: integer, description: "Character count for this platform" } - limit: { type: integer, description: "Maximum allowed characters" } - valid: { type: boolean, description: "Whether the text is within the limit" } - example: - twitter: { count: 51, limit: 280, valid: true } - twitterPremium: { count: 51, limit: 25000, valid: true } - instagram: { count: 51, limit: 2200, valid: true } - bluesky: { count: 51, limit: 300, valid: true } - snapchat: { count: 51, limit: 160, valid: true } - - /v1/tools/validate/post: - post: - operationId: validatePost - tags: [Validate] - summary: Validate post content - description: | - Dry-run the full post validation pipeline without publishing. Catches issues like missing media for Instagram/TikTok/YouTube, hashtag limits, invalid thread formats, Facebook Reel requirements, and character limit violations. - - Accepts the same body as `POST /v1/posts`. Does NOT validate accounts, process media, or track usage. This is content-only validation. - - Returns errors for failures and warnings for near-limit content (>90% of character limit). + Dry-run the full post validation pipeline without publishing. Catches issues like missing media for Instagram/TikTok/YouTube, hashtag limits, invalid thread formats, Facebook Reel requirements, and character limit violations. + + Accepts the same body as `POST /v1/posts`. Does NOT validate accounts, process media, or track usage. This is content-only validation. + + Returns errors for failures and warnings for near-limit content (>90% of character limit). security: - bearerAuth: [] requestBody: @@ -4627,9 +4428,6 @@ paths: file_too_large: value: { error: "File too large. Maximum size is 5GB." } '401': { $ref: '#/components/responses/Unauthorized' } - # Video Clips API paths removed - AI Clipping feature temporarily disabled - # /v1/video-clips, /v1/video-clips-replicate, /v1/video-clips/process, - # /v1/video-clips/jobs, /v1/video-clips/status/{jobId}, /v1/video-clips/monthly-stats /v1/reddit/search: get: operationId: searchReddit @@ -5648,6 +5446,19 @@ paths: type: string enum: [public, private, unlisted] description: Video privacy setting + thumbnailUrl: + type: string + format: uri + description: "Public URL of a custom thumbnail image (JPEG, PNG, or GIF, max 2 MB, recommended 1280x720). Works on any video you own, including existing videos not published through Zernio. The channel must be verified (phone verification) to set custom thumbnails." + madeForKids: + type: boolean + description: "COPPA compliance flag. Set true for child-directed content (restricts comments, notifications, ad targeting)." + containsSyntheticMedia: + type: boolean + description: "AI-generated content disclosure. Set true if the video contains synthetic content that could be mistaken for real. YouTube may add a label." + playlistId: + type: string + description: "YouTube playlist ID to add the video to (e.g. 'PLxxxxxxxxxxxxx'). Use GET /v1/accounts/{id}/youtube-playlists to list available playlists. Only playlists owned by the channel are supported." examples: post-based: summary: Update a video published through Zernio @@ -5664,6 +5475,13 @@ paths: accountId: "68fb37418bbca9c10cbfef26" title: "Updated Title with SEO Keywords" tags: ["seo", "youtube", "optimization"] + update-thumbnail: + summary: Update thumbnail on an existing video + value: + platform: "youtube" + videoId: "dQw4w9WgXcQ" + accountId: "68fb37418bbca9c10cbfef26" + thumbnailUrl: "https://example.com/my-thumbnail.jpg" responses: '200': description: Metadata updated successfully @@ -5959,7 +5777,9 @@ paths: operationId: listAccounts tags: [Accounts] summary: List accounts - description: Returns connected social accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on. + description: | + Returns connected social accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on. + Supports optional server-side pagination via page/limit params. When omitted, returns all accounts (backward-compatible). parameters: - name: profileId in: query @@ -5976,9 +5796,17 @@ paths: type: boolean default: false description: When true, includes accounts from over-limit profiles. + - name: page + in: query + schema: { type: integer, minimum: 1 } + description: Page number (1-based). When provided with limit, enables server-side pagination. Omit for all accounts. + - name: limit + in: query + schema: { type: integer, minimum: 1, maximum: 100 } + description: Page size. Required alongside page for pagination. responses: '200': - description: Accounts + description: Accounts (with optional pagination) content: application/json: schema: @@ -5990,6 +5818,9 @@ paths: hasAnalyticsAccess: type: boolean description: Whether user has analytics add-on access + pagination: + description: Only present when page/limit params are provided + $ref: '#/components/schemas/Pagination' examples: example: value: @@ -8862,9 +8693,7 @@ paths: username: { type: string, description: Display phone number } displayName: { type: string, description: Meta-verified business name } isActive: { type: boolean } - phoneNumber: { type: string } - verifiedName: { type: string } - qualityRating: { type: string, description: "GREEN, YELLOW, or RED" } + selectedPhoneNumber: { type: string, description: The connected phone number } example: message: "WhatsApp connected successfully" account: @@ -8873,9 +8702,7 @@ paths: username: "+1 555-123-4567" displayName: "Acme Corp" isActive: true - phoneNumber: "+1 555-123-4567" - verifiedName: "Acme Corp" - qualityRating: "GREEN" + selectedPhoneNumber: "+1 555-123-4567" '400': description: | Invalid request. Either missing fields or the phoneNumberId was not found @@ -14033,8 +13860,9 @@ paths: components: type: array description: "Template components (header, body, footer, buttons). Required for custom templates, omit when using library_template_name." + minItems: 1 items: - type: object + $ref: '#/components/schemas/WhatsAppTemplateComponent' library_template_name: type: string description: | @@ -14057,7 +13885,7 @@ paths: properties: type: type: string - enum: [QUICK_REPLY, URL, PHONE_NUMBER] + enum: [quick_reply, url, phone_number] url: type: object properties: @@ -14073,8 +13901,20 @@ paths: category: "UTILITY" language: "en_US" components: + - type: "header" + format: "image" + example: + header_handle: ["https://example.com/header.jpg"] - type: "body" text: "Your order {{1}} has been confirmed. Expected delivery: {{2}}" + example: + body_text: [["ORD-12345", "March 31"]] + - type: "footer" + text: "Thank you for your purchase" + - type: "buttons" + buttons: + - type: "quick_reply" + text: "Track Order" library: summary: Library template (pre-approved, no review) value: @@ -14084,7 +13924,7 @@ paths: language: "en_US" library_template_name: "appointment_reminder" library_template_button_inputs: - - type: "URL" + - type: "url" url: base_url: "https://myapp.com/appointments/{{1}}" responses: @@ -14186,13 +14026,20 @@ paths: components: type: array description: Updated template components + minItems: 1 items: - type: object + $ref: '#/components/schemas/WhatsAppTemplateComponent' example: accountId: "507f1f77bcf86cd799439011" components: - type: "body" text: "Updated: Your order {{1}} is confirmed. Delivery by {{2}}" + example: + body_text: [["ORD-12345", "April 1"]] + - type: "buttons" + buttons: + - type: "quick_reply" + text: "Track Order" responses: '200': description: Template updated successfully @@ -17018,3 +16865,678 @@ paths: hasMore: { type: boolean } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } + + # ── Ads ────────────────────────────────────────────────────────────────── + + /v1/ads: + get: + operationId: listAds + tags: [Ads] + summary: List ads + description: Returns a paginated list of ads with cached metrics. Use `source=all` to include externally-synced ads from platform ad managers. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/PageParam' + - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 50 } } + - { name: source, in: query, schema: { type: string, enum: [zernio, all], default: zernio }, description: "zernio = Zernio-created only, all = include external ads" } + - { name: status, in: query, schema: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error] } } + - { name: platform, in: query, schema: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } } + - { name: accountId, in: query, schema: { type: string }, description: Social account ID } + - { name: profileId, in: query, schema: { type: string }, description: Profile ID } + - { name: campaignId, in: query, schema: { type: string }, description: Platform campaign ID (filter ads within a campaign) } + responses: + '200': + description: Paginated ads + content: + application/json: + schema: + type: object + properties: + ads: + type: array + items: { $ref: '#/components/schemas/Ad' } + pagination: { $ref: '#/components/schemas/Pagination' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/campaigns: + get: + operationId: listAdCampaigns + tags: [Ad Campaigns] + summary: List campaigns with aggregate metrics + description: | + Returns campaigns as virtual aggregations over ad documents grouped by platform campaign ID. + Metrics (spend, impressions, clicks, etc.) are summed across all ads in each campaign. + Campaign status is derived from child ad statuses (active > pending_review > paused > error > completed > cancelled > rejected). + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/PageParam' + - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } + - { name: source, in: query, schema: { type: string, enum: [zernio, all], default: zernio } } + - { name: platform, in: query, schema: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } } + - { name: status, in: query, schema: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error] }, description: Filter by derived campaign status (post-aggregation) } + - { name: adAccountId, in: query, schema: { type: string }, description: Platform ad account ID (e.g. act_123 for Meta) } + - { name: accountId, in: query, schema: { type: string }, description: Social account ID } + - { name: profileId, in: query, schema: { type: string }, description: Profile ID } + responses: + '200': + description: Paginated campaigns + content: + application/json: + schema: + type: object + properties: + campaigns: + type: array + items: { $ref: '#/components/schemas/AdCampaign' } + pagination: { $ref: '#/components/schemas/Pagination' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/campaigns/{campaignId}/status: + put: + operationId: updateAdCampaignStatus + tags: [Ad Campaigns] + summary: Pause or resume a campaign + description: | + Updates the status of all ads in a campaign. Makes one platform API call (not per-ad) since status cascades through the campaign hierarchy. + Ads in terminal statuses (rejected, completed, cancelled) are automatically skipped. + security: + - bearerAuth: [] + parameters: + - { name: campaignId, in: path, required: true, schema: { type: string }, description: Platform campaign ID } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [status, platform] + properties: + status: { type: string, enum: [active, paused] } + platform: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } + responses: + '200': + description: Campaign status updated + content: + application/json: + schema: + type: object + properties: + updated: { type: integer, description: Number of ads updated } + skipped: { type: integer, description: Number of ads skipped } + skippedReasons: { type: array, items: { type: string } } + message: { type: string, description: Human-readable summary (present when no ads were actionable) } + '400': + description: Invalid input or campaign spans multiple social accounts + '401': { $ref: '#/components/responses/Unauthorized' } + '404': + description: No ads found for this campaign + + /v1/ads/tree: + get: + operationId: getAdTree + tags: [Ad Campaigns] + summary: Get nested campaign/ad-set/ad tree + description: | + Returns a nested Campaign > Ad Set > Ad hierarchy with rolled-up metrics at each level. + Uses a two-stage aggregation: ads are grouped into ad sets, then ad sets into campaigns. + Pagination is at the campaign level. Ads without a campaign or ad set ID are grouped into + synthetic "Ungrouped" buckets. + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/PageParam' + - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 }, description: Campaigns per page } + - { name: source, in: query, schema: { type: string, enum: [zernio, all], default: zernio } } + - { name: platform, in: query, schema: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter] } } + - { name: status, in: query, schema: { type: string, enum: [active, paused, pending_review, rejected, completed, cancelled, error] }, description: Filter by derived campaign status (post-aggregation) } + - { name: adAccountId, in: query, schema: { type: string }, description: Platform ad account ID } + - { name: accountId, in: query, schema: { type: string }, description: Social account ID } + - { name: profileId, in: query, schema: { type: string }, description: Profile ID } + responses: + '200': + description: Nested campaign tree with pagination + content: + application/json: + schema: + type: object + properties: + campaigns: + type: array + items: { $ref: '#/components/schemas/AdTreeCampaign' } + pagination: { $ref: '#/components/schemas/Pagination' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/{adId}: + get: + operationId: getAd + tags: [Ads] + summary: Get ad details + security: + - bearerAuth: [] + parameters: + - { name: adId, in: path, required: true, schema: { type: string } } + responses: + '200': + description: Ad details + content: + application/json: + schema: + type: object + properties: + ad: { $ref: '#/components/schemas/Ad' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + put: + operationId: updateAd + tags: [Ads] + summary: Update ad (pause/resume, budget, targeting, name) + description: Update one or more fields on an ad. Status changes and budget updates are propagated to the platform. Targeting updates are Meta-only. + security: + - bearerAuth: [] + parameters: + - { name: adId, in: path, required: true, schema: { type: string } } + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + status: { type: string, enum: [active, paused] } + budget: + type: object + properties: + amount: { type: number, description: "Minimum varies by platform: TikTok=$20, Pinterest=$5, others=$1" } + type: { type: string, enum: [daily, lifetime] } + targeting: + type: object + description: Meta-only. Targeting updates for other platforms are not supported after creation. + properties: + ageMin: { type: integer, minimum: 13, maximum: 65 } + ageMax: { type: integer, minimum: 13, maximum: 65 } + countries: { type: array, items: { type: string } } + interests: { type: array, items: { type: string } } + name: { type: string } + responses: + '200': + description: Ad updated + content: + application/json: + schema: + type: object + properties: + ad: { $ref: '#/components/schemas/Ad' } + message: { type: string } + '400': + description: Invalid status transition or budget below minimum + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + delete: + operationId: deleteAd + tags: [Ads] + summary: Cancel an ad + description: Cancels the ad on the platform and marks it as cancelled in the database. The ad is preserved for history. + security: + - bearerAuth: [] + parameters: + - { name: adId, in: path, required: true, schema: { type: string } } + responses: + '200': + description: Ad cancelled + content: + application/json: + schema: + type: object + properties: + message: { type: string } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /v1/ads/{adId}/analytics: + get: + operationId: getAdAnalytics + tags: [Ads] + summary: Get ad analytics with daily breakdown + description: | + Returns real-time analytics from the platform API (not cached). Includes summary metrics, + daily breakdown, and optional demographic breakdowns (Meta and TikTok only). + security: + - bearerAuth: [] + parameters: + - { name: adId, in: path, required: true, schema: { type: string } } + - name: breakdowns + in: query + schema: { type: string } + description: "Comma-separated breakdown dimensions. Meta: age, gender, country, publisher_platform, device_platform, region. TikTok: gender, age, country_code, platform, ac, language." + responses: + '200': + description: Ad analytics + content: + application/json: + schema: + type: object + properties: + ad: + type: object + properties: + id: { type: string } + name: { type: string } + platform: { type: string } + status: { type: string } + analytics: + type: object + properties: + summary: { $ref: '#/components/schemas/AdMetrics' } + daily: + type: array + items: + allOf: + - { $ref: '#/components/schemas/AdMetrics' } + - type: object + properties: + date: { type: string, format: date } + breakdowns: + type: object + additionalProperties: + type: array + items: { type: object } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '404': { $ref: '#/components/responses/NotFound' } + + /v1/ads/accounts: + get: + operationId: listAdAccounts + tags: [Ads] + summary: List ad accounts for a social account + description: Returns the platform ad accounts available for the given social account (e.g. Meta ad accounts, TikTok advertiser IDs, Google Ads customer IDs). + security: + - bearerAuth: [] + parameters: + - { name: accountId, in: query, required: true, schema: { type: string }, description: Social account ID } + responses: + '200': + description: Ad accounts + content: + application/json: + schema: + type: object + properties: + accounts: + type: array + items: + type: object + properties: + id: { type: string, description: "Platform ad account ID (e.g. act_123)" } + name: { type: string } + currency: { type: string } + status: { type: string } + '401': { $ref: '#/components/responses/Unauthorized' } + '422': + description: Platform ads connection required (TikTok Ads, X Ads) or Instagram missing linked Facebook account + + /v1/ads/boost: + post: + operationId: boostPost + tags: [Ads] + summary: Boost an existing post as a paid ad + description: Creates a paid ad campaign from an existing published post. Creates the full platform campaign hierarchy (campaign, ad set, ad). + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [accountId, adAccountId, name, goal, budget] + properties: + postId: { type: string, description: Zernio post ID (provide this or platformPostId) } + platformPostId: { type: string, description: Platform post ID (alternative to postId) } + accountId: { type: string, description: Social account ID } + adAccountId: { type: string, description: Platform ad account ID } + name: { type: string, maxLength: 255 } + goal: { type: string, enum: [engagement, traffic, awareness, video_views] } + budget: + type: object + required: [amount, type] + properties: + amount: { type: number, description: "Minimum varies: TikTok=$20, Pinterest=$5, others=$1" } + type: { type: string, enum: [daily, lifetime] } + currency: { type: string, example: USD } + schedule: + type: object + properties: + startDate: { type: string, format: date-time } + endDate: { type: string, format: date-time, description: Required for lifetime budgets } + targeting: + type: object + properties: + ageMin: { type: integer, minimum: 13, maximum: 65 } + ageMax: { type: integer, minimum: 13, maximum: 65 } + countries: { type: array, items: { type: string } } + interests: { type: array, items: { type: string } } + bidAmount: { type: number, description: "Max bid cap (Meta only)" } + tracking: + type: object + description: "Meta only. Tracking specs (pixel, URL tags)." + properties: + pixelId: { type: string } + urlTags: { type: string } + specialAdCategories: + type: array + description: "Meta only. Required for housing, employment, credit, or political ads." + items: { type: string, enum: [HOUSING, EMPLOYMENT, CREDIT, ISSUES_ELECTIONS_POLITICS] } + responses: + '201': + description: Ad created + content: + application/json: + schema: + type: object + properties: + ad: { $ref: '#/components/schemas/Ad' } + message: { type: string } + '400': + description: Missing required fields or invalid values + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '422': + description: Platform ads connection required (TikTok Ads, X Ads) or missing linked account + + /v1/ads/create: + post: + operationId: createStandaloneAd + tags: [Ads] + summary: Create a standalone ad with custom creative + description: Creates a paid ad with custom creative (headline, body, image/video, link). Creates the full platform campaign hierarchy. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [accountId, adAccountId, name, goal, budgetAmount, budgetType, body] + properties: + accountId: { type: string } + adAccountId: { type: string } + name: { type: string, maxLength: 255 } + goal: { type: string, enum: [engagement, traffic, awareness, video_views] } + budgetAmount: { type: number } + budgetType: { type: string, enum: [daily, lifetime] } + currency: { type: string } + headline: { type: string, description: "Required for most platforms. Max: Meta=255, Google=30, Pinterest=100" } + longHeadline: { type: string, maxLength: 90, description: "Google Display only" } + body: { type: string, description: "Max: Google=90, Pinterest=500" } + callToAction: { type: string, enum: [LEARN_MORE, SHOP_NOW, SIGN_UP, BOOK_TRAVEL, CONTACT_US, DOWNLOAD, GET_OFFER, GET_QUOTE, SUBSCRIBE, WATCH_MORE], description: Meta only } + linkUrl: { type: string, format: uri } + imageUrl: { type: string, format: uri, description: "Image URL (or video URL for TikTok). Not required for Google Search campaigns." } + businessName: { type: string, maxLength: 25, description: "Google Display only" } + boardId: { type: string, description: "Pinterest only. Board ID (auto-creates if not provided)." } + countries: { type: array, items: { type: string } } + ageMin: { type: integer, minimum: 13, maximum: 65 } + ageMax: { type: integer, minimum: 13, maximum: 65 } + interests: { type: array, items: { type: string } } + endDate: { type: string, format: date-time, description: Required for lifetime budgets } + audienceId: { type: string, description: Custom audience ID for targeting } + campaignType: { type: string, enum: [display, search], default: display, description: Google only } + keywords: { type: array, items: { type: string }, description: Google Search only } + additionalHeadlines: { type: array, items: { type: string }, description: "Google Search RSA only. Extra headlines." } + additionalDescriptions: { type: array, items: { type: string }, description: "Google Search RSA only. Extra descriptions." } + responses: + '201': + description: Ad created + content: + application/json: + schema: + type: object + properties: + ad: { $ref: '#/components/schemas/Ad' } + message: { type: string } + '400': + description: Missing required fields or invalid values + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '422': + description: Platform ads connection required (TikTok Ads, X Ads) or missing linked account + + /v1/ads/sync: + post: + operationId: syncExternalAds + tags: [Ads] + summary: Sync external ads from platform ad managers + description: Discovers and imports ads created outside Zernio (e.g. in Meta Ads Manager, Google Ads). Upserts new ads and updates metrics/status for existing ones. Also runs automatically every 30 minutes. + security: + - bearerAuth: [] + responses: + '200': + description: Sync completed + content: + application/json: + schema: + type: object + properties: + success: { type: boolean } + synced: { type: integer, description: New ads imported } + skipped: { type: integer, description: Already-known ads (skipped import, metrics still refreshed) } + errors: { type: integer, description: Failed ad imports } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/interests: + get: + operationId: searchAdInterests + tags: [Ads] + summary: Search targeting interests + description: Search for interest-based targeting options available on the platform. + security: + - bearerAuth: [] + parameters: + - { name: q, in: query, required: true, schema: { type: string }, description: Search query } + - { name: accountId, in: query, required: true, schema: { type: string }, description: Social account ID } + responses: + '200': + description: Matching interests + content: + application/json: + schema: + type: object + properties: + interests: + type: array + items: + type: object + properties: + id: { type: string } + name: { type: string } + category: { type: string } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/audiences: + get: + operationId: listAdAudiences + tags: [Ad Audiences] + summary: List custom audiences + description: Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, and Pinterest. + security: + - bearerAuth: [] + parameters: + - { name: accountId, in: query, required: true, schema: { type: string }, description: Social account ID } + - { name: adAccountId, in: query, required: true, schema: { type: string }, description: Platform ad account ID } + - { name: platform, in: query, schema: { type: string, enum: [facebook, instagram, googleads, tiktok, pinterest] } } + responses: + '200': + description: Audiences + content: + application/json: + schema: + type: object + properties: + audiences: + type: array + items: + type: object + properties: + id: { type: string, nullable: true } + platformAudienceId: { type: string } + name: { type: string } + description: { type: string } + type: { type: string, enum: [customer_list, website, lookalike] } + platform: { type: string } + size: { type: integer } + status: { type: string } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + post: + operationId: createAdAudience + tags: [Ad Audiences] + summary: Create a custom audience (Meta only) + description: Create a customer list, website retargeting, or lookalike audience on Meta (Facebook/Instagram). + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [accountId, adAccountId, name, type] + properties: + accountId: { type: string } + adAccountId: { type: string, description: "Must start with act_" } + name: { type: string, maxLength: 255 } + description: { type: string } + type: { type: string, enum: [customer_list, website, lookalike] } + pixelId: { type: string, description: Required for website audiences } + retentionDays: { type: integer, minimum: 1, maximum: 180, description: Required for website audiences } + sourceAudienceId: { type: string, description: Required for lookalike audiences } + country: { type: string, description: "2-letter code, required for lookalike audiences" } + ratio: { type: number, minimum: 0.01, maximum: 0.20, description: Required for lookalike audiences } + rule: { type: object, description: "Pixel event rule for website audiences (optional)" } + customerFileSource: { type: string, description: "Data source declaration for GDPR compliance (customer_list only)" } + responses: + '201': + description: Audience created + content: + application/json: + schema: + type: object + properties: + audience: { type: object } + message: { type: string } + '400': + description: Missing required fields + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + + /v1/ads/audiences/{audienceId}: + get: + operationId: getAdAudience + tags: [Ad Audiences] + summary: Get audience details + description: Returns the local audience record and fresh data from Meta (if available). + security: + - bearerAuth: [] + parameters: + - { name: audienceId, in: path, required: true, schema: { type: string } } + responses: + '200': + description: Audience details + content: + application/json: + schema: + type: object + properties: + audience: { type: object } + metaData: { type: object, nullable: true, description: Fresh data from Meta API } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '404': { $ref: '#/components/responses/NotFound' } + delete: + operationId: deleteAdAudience + tags: [Ad Audiences] + summary: Delete a custom audience + description: Deletes the audience from both Meta and the local database. + security: + - bearerAuth: [] + parameters: + - { name: audienceId, in: path, required: true, schema: { type: string } } + responses: + '200': + description: Audience deleted + content: + application/json: + schema: + type: object + properties: + message: { type: string } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '404': { $ref: '#/components/responses/NotFound' } + + /v1/ads/audiences/{audienceId}/users: + post: + operationId: addUsersToAdAudience + tags: [Ad Audiences] + summary: Add users to a customer list audience + description: Upload user data (emails and/or phone numbers) to a customer_list audience. Data is SHA256-hashed server-side before sending to Meta. Max 10,000 users per request. + security: + - bearerAuth: [] + parameters: + - { name: audienceId, in: path, required: true, schema: { type: string } } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [users] + properties: + users: + type: array + maxItems: 10000 + items: + type: object + properties: + email: { type: string, format: email } + phone: { type: string } + description: Each user must have at least email or phone + responses: + '200': + description: Users added + content: + application/json: + schema: + type: object + properties: + message: { type: string } + numReceived: { type: integer } + numInvalid: { type: integer } + '400': + description: Invalid input (empty users array, missing email/phone) + '401': { $ref: '#/components/responses/Unauthorized' } + '403': + description: Ads add-on required + '404': { $ref: '#/components/responses/NotFound' } + '422': + description: Audience is not a customer_list type or has no platform ID yet diff --git a/pyproject.toml b/pyproject.toml index 66ab77a..790d607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.3.35" +version = "1.3.36" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 15adfa1..8769c1a 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.3.35" +__version__ = "1.3.36" __all__ = [ # Client diff --git a/src/late/mcp/generated_tools.py b/src/late/mcp/generated_tools.py index c0abd9f..165ef2a 100644 --- a/src/late/mcp/generated_tools.py +++ b/src/late/mcp/generated_tools.py @@ -258,20 +258,28 @@ def account_settings_delete_telegram_commands(account_id: str) -> str: @mcp.tool() def accounts_list_accounts( - profile_id: str = "", platform: str = "", include_over_limit: bool = False + profile_id: str = "", + platform: str = "", + include_over_limit: bool = False, + page: int = 0, + limit: int = 0, ) -> str: """List accounts Args: profile_id: Filter accounts by profile ID platform: Filter accounts by platform (e.g. "instagram", "twitter"). - include_over_limit: When true, includes accounts from over-limit profiles.""" + include_over_limit: When true, includes accounts from over-limit profiles. + page: Page number (1-based). When provided with limit, enables server-side pagination. Omit for all accounts. + limit: Page size. Required alongside page for pagination.""" client = _get_client() try: response = client.accounts.list_accounts( profile_id=profile_id, platform=platform, include_over_limit=include_over_limit, + page=page, + limit=limit, ) return _format_response(response) except Exception as e: @@ -723,6 +731,504 @@ def accounts_get_linked_in_mentions( except Exception as e: return f"Error: {e}" + # AD_AUDIENCES + + @mcp.tool() + def ad_audiences_list_ad_audiences( + account_id: str, ad_account_id: str, platform: str = "" + ) -> str: + """List custom audiences + + Args: + account_id: Social account ID (required) + ad_account_id: Platform ad account ID (required) + platform""" + client = _get_client() + try: + response = client.ad_audiences.list_ad_audiences( + account_id=account_id, ad_account_id=ad_account_id, platform=platform + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_audiences_create_ad_audience( + account_id: str, + ad_account_id: str, + name: str, + type: str, + description: str = "", + pixel_id: str = "", + retention_days: int = 0, + source_audience_id: str = "", + country: str = "", + ratio: float = 0.0, + rule: str = "", + customer_file_source: str = "", + ) -> str: + """Create a custom audience (Meta only) + + Args: + account_id: (required) + ad_account_id: Must start with act_ (required) + name: (required) + description + type: (required) + pixel_id: Required for website audiences + retention_days: Required for website audiences + source_audience_id: Required for lookalike audiences + country: 2-letter code, required for lookalike audiences + ratio: Required for lookalike audiences + rule: Pixel event rule for website audiences (optional) + customer_file_source: Data source declaration for GDPR compliance (customer_list only)""" + client = _get_client() + try: + response = client.ad_audiences.create_ad_audience( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + description=description, + type=type, + pixel_id=pixel_id, + retention_days=retention_days, + source_audience_id=source_audience_id, + country=country, + ratio=ratio, + rule=rule, + customer_file_source=customer_file_source, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_audiences_get_ad_audience(audience_id: str) -> str: + """Get audience details + + Args: + audience_id: (required)""" + client = _get_client() + try: + response = client.ad_audiences.get_ad_audience(audience_id=audience_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_audiences_delete_ad_audience(audience_id: str) -> str: + """Delete a custom audience + + Args: + audience_id: (required)""" + client = _get_client() + try: + response = client.ad_audiences.delete_ad_audience(audience_id=audience_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_audiences_add_users_to_ad_audience(audience_id: str, users: str) -> str: + """Add users to a customer list audience + + Args: + audience_id: (required) + users: (required)""" + client = _get_client() + try: + response = client.ad_audiences.add_users_to_ad_audience( + audience_id=audience_id, users=users + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + # AD_CAMPAIGNS + + @mcp.tool() + def ad_campaigns_list_ad_campaigns( + page: int = 1, + limit: int = 20, + source: str = "zernio", + platform: str = "", + status: str = "", + ad_account_id: str = "", + account_id: str = "", + profile_id: str = "", + ) -> str: + """List campaigns with aggregate metrics + + Args: + page: Page number + limit + source + platform + status: Filter by derived campaign status (post-aggregation) + ad_account_id: Platform ad account ID (e.g. act_123 for Meta) + account_id: Social account ID + profile_id: Profile ID""" + client = _get_client() + try: + response = client.ad_campaigns.list_ad_campaigns( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_campaigns_update_ad_campaign_status( + campaign_id: str, status: str, platform: str + ) -> str: + """Pause or resume a campaign + + Args: + campaign_id: Platform campaign ID (required) + status: (required) + platform: (required)""" + client = _get_client() + try: + response = client.ad_campaigns.update_ad_campaign_status( + campaign_id=campaign_id, status=status, platform=platform + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ad_campaigns_get_ad_tree( + page: int = 1, + limit: int = 20, + source: str = "zernio", + platform: str = "", + status: str = "", + ad_account_id: str = "", + account_id: str = "", + profile_id: str = "", + ) -> str: + """Get nested campaign/ad-set/ad tree + + Args: + page: Page number + limit: Campaigns per page + source + platform + status: Filter by derived campaign status (post-aggregation) + ad_account_id: Platform ad account ID + account_id: Social account ID + profile_id: Profile ID""" + client = _get_client() + try: + response = client.ad_campaigns.get_ad_tree( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + # ADS + + @mcp.tool() + def ads_list_ads( + page: int = 1, + limit: int = 50, + source: str = "zernio", + status: str = "", + platform: str = "", + account_id: str = "", + profile_id: str = "", + campaign_id: str = "", + ) -> str: + """List ads + + Args: + page: Page number + limit + source: zernio = Zernio-created only, all = include external ads + status + platform + account_id: Social account ID + profile_id: Profile ID + campaign_id: Platform campaign ID (filter ads within a campaign)""" + client = _get_client() + try: + response = client.ads.list_ads( + page=page, + limit=limit, + source=source, + status=status, + platform=platform, + account_id=account_id, + profile_id=profile_id, + campaign_id=campaign_id, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_get_ad(ad_id: str) -> str: + """Get ad details + + Args: + ad_id: (required)""" + client = _get_client() + try: + response = client.ads.get_ad(ad_id=ad_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_update_ad( + ad_id: str, + status: str = "", + budget: str = "", + targeting: str = "", + name: str = "", + ) -> str: + """Update ad (pause/resume, budget, targeting, name) + + Args: + ad_id: (required) + status + budget + targeting: Meta-only. Targeting updates for other platforms are not supported after creation. + name""" + client = _get_client() + try: + response = client.ads.update_ad( + ad_id=ad_id, + status=status, + budget=budget, + targeting=targeting, + name=name, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_delete_ad(ad_id: str) -> str: + """Cancel an ad + + Args: + ad_id: (required)""" + client = _get_client() + try: + response = client.ads.delete_ad(ad_id=ad_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_get_ad_analytics(ad_id: str, breakdowns: str = "") -> str: + """Get ad analytics with daily breakdown + + Args: + ad_id: (required) + breakdowns: Comma-separated breakdown dimensions. Meta: age, gender, country, publisher_platform, device_platform, region. TikTok: gender, age, country_code, platform, ac, language.""" + client = _get_client() + try: + response = client.ads.get_ad_analytics(ad_id=ad_id, breakdowns=breakdowns) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_list_ad_accounts(account_id: str) -> str: + """List ad accounts for a social account + + Args: + account_id: Social account ID (required)""" + client = _get_client() + try: + response = client.ads.list_ad_accounts(account_id=account_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_boost_post( + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget: str, + post_id: str = "", + platform_post_id: str = "", + currency: str = "", + schedule: str = "", + targeting: str = "", + bid_amount: float = 0.0, + tracking: str = "", + special_ad_categories: str = "", + ) -> str: + """Boost an existing post as a paid ad + + Args: + post_id: Zernio post ID (provide this or platformPostId) + platform_post_id: Platform post ID (alternative to postId) + account_id: Social account ID (required) + ad_account_id: Platform ad account ID (required) + name: (required) + goal: (required) + budget: (required) + currency + schedule + targeting + bid_amount: Max bid cap (Meta only) + tracking: Meta only. Tracking specs (pixel, URL tags). + special_ad_categories: Meta only. Required for housing, employment, credit, or political ads.""" + client = _get_client() + try: + response = client.ads.boost_post( + post_id=post_id, + platform_post_id=platform_post_id, + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget=budget, + currency=currency, + schedule=schedule, + targeting=targeting, + bid_amount=bid_amount, + tracking=tracking, + special_ad_categories=special_ad_categories, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_create_standalone_ad( + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget_amount: float, + budget_type: str, + body: str, + currency: str = "", + headline: str = "", + long_headline: str = "", + call_to_action: str = "", + link_url: str = "", + image_url: str = "", + business_name: str = "", + board_id: str = "", + countries: str = "", + age_min: int = 0, + age_max: int = 0, + interests: str = "", + end_date: str = "", + audience_id: str = "", + campaign_type: str = "display", + keywords: str = "", + additional_headlines: str = "", + additional_descriptions: str = "", + ) -> str: + """Create a standalone ad with custom creative + + Args: + account_id: (required) + ad_account_id: (required) + name: (required) + goal: (required) + budget_amount: (required) + budget_type: (required) + currency + headline: Required for most platforms. Max: Meta=255, Google=30, Pinterest=100 + long_headline: Google Display only + body: Max: Google=90, Pinterest=500 (required) + call_to_action: Meta only + link_url + image_url: Image URL (or video URL for TikTok). Not required for Google Search campaigns. + business_name: Google Display only + board_id: Pinterest only. Board ID (auto-creates if not provided). + countries + age_min + age_max + interests + end_date: Required for lifetime budgets + audience_id: Custom audience ID for targeting + campaign_type: Google only + keywords: Google Search only + additional_headlines: Google Search RSA only. Extra headlines. + additional_descriptions: Google Search RSA only. Extra descriptions.""" + client = _get_client() + try: + response = client.ads.create_standalone_ad( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget_amount=budget_amount, + budget_type=budget_type, + currency=currency, + headline=headline, + long_headline=long_headline, + body=body, + call_to_action=call_to_action, + link_url=link_url, + image_url=image_url, + business_name=business_name, + board_id=board_id, + countries=countries, + age_min=age_min, + age_max=age_max, + interests=interests, + end_date=end_date, + audience_id=audience_id, + campaign_type=campaign_type, + keywords=keywords, + additional_headlines=additional_headlines, + additional_descriptions=additional_descriptions, + ) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_sync_external_ads() -> str: + """Sync external ads from platform ad managers""" + client = _get_client() + try: + response = client.ads.sync_external_ads() + return _format_response(response) + except Exception as e: + return f"Error: {e}" + + @mcp.tool() + def ads_search_ad_interests(q: str, account_id: str) -> str: + """Search targeting interests + + Args: + q: Search query (required) + account_id: Social account ID (required)""" + client = _get_client() + try: + response = client.ads.search_ad_interests(q=q, account_id=account_id) + return _format_response(response) + except Exception as e: + return f"Error: {e}" + # ANALYTICS @mcp.tool() @@ -3074,6 +3580,10 @@ def posts_update_post_metadata( tags: str = "", category_id: str = "", privacy_status: str = "", + thumbnail_url: str = "", + made_for_kids: bool = False, + contains_synthetic_media: bool = False, + playlist_id: str = "", ) -> str: """Update post metadata @@ -3086,7 +3596,11 @@ def posts_update_post_metadata( description: New video description tags: Array of keyword tags (max 500 characters combined for YouTube) category_id: YouTube video category ID - privacy_status: Video privacy setting""" + privacy_status: Video privacy setting + thumbnail_url: Public URL of a custom thumbnail image (JPEG, PNG, or GIF, max 2 MB, recommended 1280x720). Works on any video you own, including existing videos not published through Zernio. The channel must be verified (phone verification) to set custom thumbnails. + made_for_kids: COPPA compliance flag. Set true for child-directed content (restricts comments, notifications, ad targeting). + contains_synthetic_media: AI-generated content disclosure. Set true if the video contains synthetic content that could be mistaken for real. YouTube may add a label. + playlist_id: YouTube playlist ID to add the video to (e.g. 'PLxxxxxxxxxxxxx'). Use GET /v1/accounts/{id}/youtube-playlists to list available playlists. Only playlists owned by the channel are supported.""" client = _get_client() try: response = client.posts.update_post_metadata( @@ -3099,6 +3613,10 @@ def posts_update_post_metadata( tags=tags, category_id=category_id, privacy_status=privacy_status, + thumbnail_url=thumbnail_url, + made_for_kids=made_for_kids, + contains_synthetic_media=contains_synthetic_media, + playlist_id=playlist_id, ) return _format_response(response) except Exception as e: @@ -3662,154 +4180,6 @@ def sequences_list_sequence_enrollments( except Exception as e: return f"Error: {e}" - # TOOLS - - @mcp.tool() - def tools_download_you_tube_video( - url: str, - action: str = "download", - format: str = "video", - quality: str = "hd", - format_id: str = "", - ) -> str: - """Download YouTube video - - Args: - url: YouTube video URL or video ID (required) - action: Action to perform: 'download' returns download URL, 'formats' lists available formats - format: Desired format (when action=download) - quality: Desired quality (when action=download) - format_id: Specific format ID from formats list""" - client = _get_client() - try: - response = client.tools.download_you_tube_video( - url=url, - action=action, - format=format, - quality=quality, - format_id=format_id, - ) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_get_you_tube_transcript(url: str, lang: str = "en") -> str: - """Get YouTube transcript - - Args: - url: YouTube video URL or video ID (required) - lang: Language code for transcript""" - client = _get_client() - try: - response = client.tools.get_you_tube_transcript(url=url, lang=lang) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_instagram_media(url: str) -> str: - """Download Instagram media - - Args: - url: Instagram reel or post URL (required)""" - client = _get_client() - try: - response = client.tools.download_instagram_media(url=url) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_check_instagram_hashtags(hashtags: str) -> str: - """Check IG hashtag bans - - Args: - hashtags: (required)""" - client = _get_client() - try: - response = client.tools.check_instagram_hashtags(hashtags=hashtags) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_tik_tok_video( - url: str, action: str = "download", format_id: str = "" - ) -> str: - """Download TikTok video - - Args: - url: TikTok video URL or ID (required) - action: 'formats' to list available formats - format_id: Specific format ID (0 = no watermark, etc.)""" - client = _get_client() - try: - response = client.tools.download_tik_tok_video( - url=url, action=action, format_id=format_id - ) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_twitter_media( - url: str, action: str = "download", format_id: str = "" - ) -> str: - """Download Twitter/X media - - Args: - url: Twitter/X post URL (required) - action - format_id""" - client = _get_client() - try: - response = client.tools.download_twitter_media( - url=url, action=action, format_id=format_id - ) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_facebook_video(url: str) -> str: - """Download Facebook video - - Args: - url: Facebook video or reel URL (required)""" - client = _get_client() - try: - response = client.tools.download_facebook_video(url=url) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_linked_in_video(url: str) -> str: - """Download LinkedIn video - - Args: - url: LinkedIn post URL (required)""" - client = _get_client() - try: - response = client.tools.download_linked_in_video(url=url) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - - @mcp.tool() - def tools_download_bluesky_media(url: str) -> str: - """Download Bluesky media - - Args: - url: Bluesky post URL (required)""" - client = _get_client() - try: - response = client.tools.download_bluesky_media(url=url) - return _format_response(response) - except Exception as e: - return f"Error: {e}" - # TWITTER_ENGAGEMENT @mcp.tool() diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index aafea82..35e2b5f 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-03-30T16:57:51+00:00 +# timestamp: 2026-04-02T14:37:24+00:00 from __future__ import annotations from datetime import date as date_aliased from enum import Enum -from typing import Annotated, Any, Dict, List +from typing import Annotated, Any, Dict, List, Literal from pydantic import AnyUrl, AwareDatetime, BaseModel, Field, RootModel @@ -16,6 +16,138 @@ class ErrorResponse(BaseModel): details: Dict[str, Any] | None = None +class Type(Enum): + QUICK_REPLY = "quick_reply" + URL = "url" + PHONE_NUMBER = "phone_number" + OTP = "otp" + FLOW = "flow" + MPM = "mpm" + CATALOG = "catalog" + + +class OtpType(Enum): + """ + Required when type is otp + """ + + COPY_CODE = "copy_code" + ONE_TAP = "one_tap" + ZERO_TAP = "zero_tap" + + +class WhatsAppTemplateButton(BaseModel): + type: Type + text: str + url: AnyUrl | None = None + """ + Required when type is URL + """ + example: List[str] | None = None + """ + Example values for URL suffix variables + """ + phone_number: str | None = None + """ + Required when type is phone_number + """ + otp_type: OtpType | None = None + """ + Required when type is otp + """ + autofill_text: str | None = None + package_name: str | None = None + signature_hash: str | None = None + flow_id: str | None = None + flow_name: str | None = None + flow_json: str | None = None + flow_action: str | None = None + navigate_screen: str | None = None + + +class Type1(Enum): + HEADER = "header" + + +class Format(Enum): + TEXT = "text" + IMAGE = "image" + VIDEO = "video" + GIF = "gif" + DOCUMENT = "document" + LOCATION = "location" + + +class Example(BaseModel): + header_text: List[str] | None = None + """ + Sample values for header text variables + """ + header_handle: List[AnyUrl] | None = None + """ + When the header format is a media type (image, video, gif, document), provide a public URL here. Zernio will download and upload it to WhatsApp on your behalf, replacing it with the internal file handle before creating the template. + """ + + +class WhatsAppHeaderComponent(BaseModel): + type: Literal["header"] + format: Format + text: str | None = None + """ + Header text (may include {{1}} variable). Used when format is TEXT. + """ + example: Example | None = None + + +class Type2(Enum): + BODY = "body" + + +class Example1(BaseModel): + body_text: List[List[str]] | None = None + """ + Sample values for body variables (array of arrays) + """ + + +class WhatsAppBodyComponent(BaseModel): + type: Literal["body"] + text: str + """ + Body text with optional {{n}} variables + """ + add_security_recommendation: bool | None = None + """ + Add security recommendation text (authentication templates only) + """ + example: Example1 | None = None + + +class Type3(Enum): + FOOTER = "footer" + + +class WhatsAppFooterComponent(BaseModel): + type: Literal["footer"] + text: str | None = None + """ + Static footer text + """ + code_expiration_minutes: Annotated[int | None, Field(ge=1)] = None + """ + OTP code expiry in minutes (authentication templates only) + """ + + +class Type4(Enum): + BUTTONS = "buttons" + + +class WhatsAppButtonsComponent(BaseModel): + type: Literal["buttons"] + buttons: Annotated[List[WhatsAppTemplateButton], Field(min_length=1)] + + class FoodMenuLabel(BaseModel): displayName: str """ @@ -1041,7 +1173,7 @@ class ConnectionLog(BaseModel): createdAt: AwareDatetime | None = None -class Type(Enum): +class Type5(Enum): IMAGE = "image" VIDEO = "video" GIF = "gif" @@ -1053,7 +1185,7 @@ class MediaItem(BaseModel): Media referenced in posts. URLs must be publicly reachable over HTTPS. Use POST /v1/media/presign for uploads up to 5GB. Zernio auto-compresses images and videos that exceed platform limits (videos over 200 MB may not be compressed). """ - type: Type | None = None + type: Type5 | None = None url: AnyUrl | None = None title: str | None = None """ @@ -1515,7 +1647,7 @@ class YouTubePlatformData(BaseModel): """ -class Type1(Enum): +class Type6(Enum): """ Button action type: LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL """ @@ -1533,7 +1665,7 @@ class CallToAction(BaseModel): Optional call-to-action button displayed on the post """ - type: Type1 + type: Type6 """ Button action type: LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL """ @@ -1834,6 +1966,18 @@ class SocialAccount(BaseModel): """ Last time follower count was updated (only included if user has analytics add-on) """ + metadata: Dict[str, Any] | None = None + """ + Platform-specific metadata. Fields vary by platform. For WhatsApp accounts, includes: + - `qualityRating`: Phone number quality rating from Meta (`GREEN`, `YELLOW`, `RED`, or `UNKNOWN`) + - `nameStatus`: Display name review status (`APPROVED`, `PENDING_REVIEW`, `DECLINED`, or `NONE`). Messages cannot be sent until the display name is approved by Meta. + - `messagingLimitTier`: Maximum unique business-initiated conversations per 24h rolling window (`TIER_250`, `TIER_1K`, `TIER_10K`, `TIER_100K`, or `TIER_UNLIMITED`). Scales automatically as quality rating improves. + - `verifiedName`: Meta-verified business display name + - `displayPhoneNumber`: Formatted phone number (e.g., "+1 555-123-4567") + - `wabaId`: WhatsApp Business Account ID + - `phoneNumberId`: Meta phone number ID + + """ class AccountStats(BaseModel): @@ -2103,13 +2247,13 @@ class MediaType1(Enum): TEXT = "text" -class Type2(Enum): +class Type7(Enum): IMAGE = "image" VIDEO = "video" class MediaItem1(BaseModel): - type: Type2 | None = None + type: Type7 | None = None url: AnyUrl | None = None """ Direct URL to the media @@ -2164,7 +2308,7 @@ class MediaType2(Enum): class MediaItem2(BaseModel): - type: Type2 | None = None + type: Type7 | None = None url: AnyUrl | None = None """ Direct URL to the media @@ -2378,14 +2522,14 @@ class FollowerStatsResponse(BaseModel): aggregation: Aggregation2 | None = None -class Type4(Enum): +class Type9(Enum): IMAGE = "image" VIDEO = "video" DOCUMENT = "document" class UploadedFile(BaseModel): - type: Type4 | None = None + type: Type9 | None = None url: AnyUrl | None = None filename: str | None = None size: int | None = None @@ -2448,69 +2592,268 @@ class QueueNextSlotResponse(BaseModel): timezone: str | None = None -class DownloadFormat(BaseModel): - formatId: str | None = None - ext: str | None = None - resolution: str | None = None - filesize: int | None = None - quality: str | None = None +class User(BaseModel): + field_id: Annotated[str | None, Field(alias="_id")] = None + email: str | None = None + name: str | None = None + role: str | None = None + createdAt: AwareDatetime | None = None -class DownloadResponse(BaseModel): - url: AnyUrl | None = None - title: str | None = None - thumbnail: AnyUrl | None = None - duration: int | None = None - formats: List[DownloadFormat] | None = None +class UsersListResponse(BaseModel): + users: List[User] | None = None -class TranscriptSegment(BaseModel): - text: str | None = None - start: float | None = None - duration: float | None = None +class UserGetResponse(BaseModel): + user: User | None = None -class TranscriptResponse(BaseModel): - transcript: str | None = None - segments: List[TranscriptSegment] | None = None - language: str | None = None +class AdMetrics(BaseModel): + spend: float | None = None + impressions: int | None = None + reach: int | None = None + clicks: int | None = None + ctr: float | None = None + """ + Click-through rate (%) + """ + cpc: float | None = None + """ + Cost per click + """ + cpm: float | None = None + """ + Cost per 1000 impressions + """ + engagement: int | None = None + lastSyncedAt: AwareDatetime | None = None + """ + Present on individual ads only, not on campaign aggregations + """ + + +class Platform5(Enum): + FACEBOOK = "facebook" + INSTAGRAM = "instagram" + TIKTOK = "tiktok" + LINKEDIN = "linkedin" + PINTEREST = "pinterest" + GOOGLE = "google" + TWITTER = "twitter" class Status8(Enum): - SAFE = "safe" - BANNED = "banned" - RESTRICTED = "restricted" - UNKNOWN = "unknown" + ACTIVE = "active" + PAUSED = "paused" + PENDING_REVIEW = "pending_review" + REJECTED = "rejected" + COMPLETED = "completed" + CANCELLED = "cancelled" + ERROR = "error" -class HashtagInfo(BaseModel): - hashtag: str | None = None - status: Status8 | None = None - postCount: int | None = None +class AdType(Enum): + BOOST = "boost" + STANDALONE = "standalone" -class HashtagCheckResponse(BaseModel): - hashtags: List[HashtagInfo] | None = None +class Goal(Enum): + ENGAGEMENT = "engagement" + TRAFFIC = "traffic" + AWARENESS = "awareness" + VIDEO_VIEWS = "video_views" -class CaptionResponse(BaseModel): - caption: str | None = None +class Type10(Enum): + DAILY = "daily" + LIFETIME = "lifetime" -class User(BaseModel): +class Budget(BaseModel): + amount: float | None = None + type: Type10 | None = None + + +class Creative(BaseModel): + """ + Platform-specific creative data. Fields vary by platform. + """ + + thumbnailUrl: str | None = None + """ + Primary thumbnail/image URL + """ + imageUrl: str | None = None + """ + Alternative image URL + """ + mediaUrls: List[str] | None = None + """ + All media URLs for this ad (carousel images, multiple assets). Populated for Meta (carousel child_attachments), Google Ads (responsive display marketing_images), and LinkedIn (multi-image posts). + """ + body: str | None = None + """ + Ad copy/text + """ + googleHeadline: str | None = None + """ + Google Ads headline + """ + googleDescription: str | None = None + """ + Google Ads description + """ + linkUrl: str | None = None + """ + Destination URL + """ + pinterestImageUrl: str | None = None + pinterestTitle: str | None = None + pinterestDescription: str | None = None + + +class Schedule(BaseModel): + startDate: AwareDatetime | None = None + endDate: AwareDatetime | None = None + + +class Ad(BaseModel): field_id: Annotated[str | None, Field(alias="_id")] = None - email: str | None = None name: str | None = None - role: str | None = None + platform: Platform5 | None = None + status: Status8 | None = None + adType: AdType | None = None + goal: Goal | None = None + isExternal: bool | None = None + """ + True for ads synced from platform ad managers + """ + budget: Budget | None = None + metrics: AdMetrics | None = None + platformAdId: str | None = None + platformAdAccountId: str | None = None + platformCampaignId: str | None = None + platformAdSetId: str | None = None + campaignName: str | None = None + adSetName: str | None = None + creative: Creative | None = None + """ + Platform-specific creative data. Fields vary by platform. + """ + targeting: Dict[str, Any] | None = None + schedule: Schedule | None = None + rejectionReason: str | None = None createdAt: AwareDatetime | None = None + updatedAt: AwareDatetime | None = None -class UsersListResponse(BaseModel): - users: List[User] | None = None +class Status9(Enum): + """ + Derived from child ad statuses + """ + + ACTIVE = "active" + PAUSED = "paused" + PENDING_REVIEW = "pending_review" + REJECTED = "rejected" + COMPLETED = "completed" + CANCELLED = "cancelled" + ERROR = "error" -class UserGetResponse(BaseModel): - user: User | None = None +class Budget1(BaseModel): + amount: float | None = None + type: Type10 | None = None + + +class AdTreeAdSet(BaseModel): + """ + Ad set (or ad group/line item depending on platform) with rolled-up metrics and child ads + """ + + platformAdSetId: str | None = None + adSetName: str | None = None + status: Status9 | None = None + """ + Derived from child ad statuses + """ + adCount: int | None = None + budget: Budget1 | None = None + metrics: AdMetrics | None = None + ads: List[Ad] | None = None + """ + Individual ads within this ad set (capped at 100). Returns a subset of Ad fields from the aggregation (core fields like _id, name, platform, status, budget, metrics, creative, goal are included; targeting and schedule may be absent). + """ + + +class Budget2(BaseModel): + amount: float | None = None + type: Type10 | None = None + + +class AdTreeCampaign(BaseModel): + """ + Campaign with nested ad sets and rolled-up metrics + """ + + platformCampaignId: str | None = None + platform: Platform5 | None = None + campaignName: str | None = None + status: Status9 | None = None + """ + Derived from child ad statuses + """ + adCount: int | None = None + """ + Total ads across all ad sets + """ + adSetCount: int | None = None + budget: Budget2 | None = None + metrics: AdMetrics | None = None + platformAdAccountId: str | None = None + accountId: str | None = None + profileId: str | None = None + adSets: List[AdTreeAdSet] | None = None + + +class Budget3(BaseModel): + amount: float | None = None + type: Type10 | None = None + + +class AdCampaign(BaseModel): + platformCampaignId: str | None = None + platform: Platform5 | None = None + campaignName: str | None = None + status: Status9 | None = None + """ + Derived from child ad statuses + """ + adCount: int | None = None + budget: Budget3 | None = None + metrics: AdMetrics | None = None + platformAdAccountId: str | None = None + accountId: str | None = None + profileId: str | None = None + earliestAd: AwareDatetime | None = None + latestAd: AwareDatetime | None = None + + +class WhatsAppTemplateComponent( + RootModel[ + WhatsAppHeaderComponent + | WhatsAppBodyComponent + | WhatsAppFooterComponent + | WhatsAppButtonsComponent + ] +): + root: Annotated[ + WhatsAppHeaderComponent + | WhatsAppBodyComponent + | WhatsAppFooterComponent + | WhatsAppButtonsComponent, + Field(discriminator="type"), + ] class PlatformTarget(BaseModel): diff --git a/src/late/resources/_generated/__init__.py b/src/late/resources/_generated/__init__.py index 8b93c80..e5f7c09 100644 --- a/src/late/resources/_generated/__init__.py +++ b/src/late/resources/_generated/__init__.py @@ -5,6 +5,9 @@ from .account_groups import AccountGroupsResource from .account_settings import AccountSettingsResource from .accounts import AccountsResource +from .ad_audiences import AdAudiencesResource +from .ad_campaigns import AdCampaignsResource +from .ads import AdsResource from .analytics import AnalyticsResource from .api_keys import ApiKeysResource from .broadcasts import BroadcastsResource @@ -23,7 +26,6 @@ from .reddit import RedditResource from .reviews import ReviewsResource from .sequences import SequencesResource -from .tools import ToolsResource from .twitter_engagement import TwitterEngagementResource from .usage import UsageResource from .users import UsersResource @@ -36,6 +38,9 @@ "AccountGroupsResource", "AccountSettingsResource", "AccountsResource", + "AdAudiencesResource", + "AdCampaignsResource", + "AdsResource", "AnalyticsResource", "ApiKeysResource", "BroadcastsResource", @@ -54,7 +59,6 @@ "RedditResource", "ReviewsResource", "SequencesResource", - "ToolsResource", "TwitterEngagementResource", "UsageResource", "UsersResource", diff --git a/src/late/resources/_generated/accounts.py b/src/late/resources/_generated/accounts.py index 44d7272..91292c1 100644 --- a/src/late/resources/_generated/accounts.py +++ b/src/late/resources/_generated/accounts.py @@ -54,12 +54,16 @@ def list_accounts( profile_id: str | None = None, platform: str | None = None, include_over_limit: bool | None = False, + page: int | None = None, + limit: int | None = None, ) -> dict[str, Any]: """List accounts""" params = self._build_params( profile_id=profile_id, platform=platform, include_over_limit=include_over_limit, + page=page, + limit=limit, ) return self._client._get("/v1/accounts", params=params) @@ -361,12 +365,16 @@ async def alist_accounts( profile_id: str | None = None, platform: str | None = None, include_over_limit: bool | None = False, + page: int | None = None, + limit: int | None = None, ) -> dict[str, Any]: """List accounts (async)""" params = self._build_params( profile_id=profile_id, platform=platform, include_over_limit=include_over_limit, + page=page, + limit=limit, ) return await self._client._aget("/v1/accounts", params=params) diff --git a/src/late/resources/_generated/ad_audiences.py b/src/late/resources/_generated/ad_audiences.py new file mode 100644 index 0000000..60f1f47 --- /dev/null +++ b/src/late/resources/_generated/ad_audiences.py @@ -0,0 +1,175 @@ +""" +Auto-generated ad_audiences resource. + +DO NOT EDIT THIS FILE MANUALLY. +Run `python scripts/generate_resources.py` to regenerate. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ..client.base import BaseClient + + +class AdAudiencesResource: + """ + ad_audiences operations. + """ + + def __init__(self, client: BaseClient) -> None: + self._client = client + + def _build_params(self, **kwargs: Any) -> dict[str, Any]: + """Build query parameters, filtering None values.""" + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + return {to_camel(k): v for k, v in kwargs.items() if v is not None} + + def _build_payload(self, **kwargs: Any) -> dict[str, Any]: + """Build request payload, filtering None values.""" + from datetime import datetime + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if v is None: + continue + if isinstance(v, datetime): + result[to_camel(k)] = v.isoformat() + else: + result[to_camel(k)] = v + return result + + def list_ad_audiences( + self, account_id: str, ad_account_id: str, *, platform: str | None = None + ) -> dict[str, Any]: + """List custom audiences""" + params = self._build_params( + account_id=account_id, + ad_account_id=ad_account_id, + platform=platform, + ) + return self._client._get("/v1/ads/audiences", params=params) + + def create_ad_audience( + self, + account_id: str, + ad_account_id: str, + name: str, + type: str, + *, + description: str | None = None, + pixel_id: str | None = None, + retention_days: int | None = None, + source_audience_id: str | None = None, + country: str | None = None, + ratio: float | None = None, + rule: dict[str, Any] | None = None, + customer_file_source: str | None = None, + ) -> dict[str, Any]: + """Create a custom audience (Meta only)""" + payload = self._build_payload( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + description=description, + type=type, + pixel_id=pixel_id, + retention_days=retention_days, + source_audience_id=source_audience_id, + country=country, + ratio=ratio, + rule=rule, + customer_file_source=customer_file_source, + ) + return self._client._post("/v1/ads/audiences", data=payload) + + def get_ad_audience(self, audience_id: str) -> dict[str, Any]: + """Get audience details""" + return self._client._get(f"/v1/ads/audiences/{audience_id}") + + def delete_ad_audience(self, audience_id: str) -> dict[str, Any]: + """Delete a custom audience""" + return self._client._delete(f"/v1/ads/audiences/{audience_id}") + + def add_users_to_ad_audience( + self, audience_id: str, users: list[dict[str, Any]] + ) -> dict[str, Any]: + """Add users to a customer list audience""" + payload = self._build_payload( + users=users, + ) + return self._client._post( + f"/v1/ads/audiences/{audience_id}/users", data=payload + ) + + async def alist_ad_audiences( + self, account_id: str, ad_account_id: str, *, platform: str | None = None + ) -> dict[str, Any]: + """List custom audiences (async)""" + params = self._build_params( + account_id=account_id, + ad_account_id=ad_account_id, + platform=platform, + ) + return await self._client._aget("/v1/ads/audiences", params=params) + + async def acreate_ad_audience( + self, + account_id: str, + ad_account_id: str, + name: str, + type: str, + *, + description: str | None = None, + pixel_id: str | None = None, + retention_days: int | None = None, + source_audience_id: str | None = None, + country: str | None = None, + ratio: float | None = None, + rule: dict[str, Any] | None = None, + customer_file_source: str | None = None, + ) -> dict[str, Any]: + """Create a custom audience (Meta only) (async)""" + payload = self._build_payload( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + description=description, + type=type, + pixel_id=pixel_id, + retention_days=retention_days, + source_audience_id=source_audience_id, + country=country, + ratio=ratio, + rule=rule, + customer_file_source=customer_file_source, + ) + return await self._client._apost("/v1/ads/audiences", data=payload) + + async def aget_ad_audience(self, audience_id: str) -> dict[str, Any]: + """Get audience details (async)""" + return await self._client._aget(f"/v1/ads/audiences/{audience_id}") + + async def adelete_ad_audience(self, audience_id: str) -> dict[str, Any]: + """Delete a custom audience (async)""" + return await self._client._adelete(f"/v1/ads/audiences/{audience_id}") + + async def aadd_users_to_ad_audience( + self, audience_id: str, users: list[dict[str, Any]] + ) -> dict[str, Any]: + """Add users to a customer list audience (async)""" + payload = self._build_payload( + users=users, + ) + return await self._client._apost( + f"/v1/ads/audiences/{audience_id}/users", data=payload + ) diff --git a/src/late/resources/_generated/ad_campaigns.py b/src/late/resources/_generated/ad_campaigns.py new file mode 100644 index 0000000..88361bb --- /dev/null +++ b/src/late/resources/_generated/ad_campaigns.py @@ -0,0 +1,173 @@ +""" +Auto-generated ad_campaigns resource. + +DO NOT EDIT THIS FILE MANUALLY. +Run `python scripts/generate_resources.py` to regenerate. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ..client.base import BaseClient + + +class AdCampaignsResource: + """ + ad_campaigns operations. + """ + + def __init__(self, client: BaseClient) -> None: + self._client = client + + def _build_params(self, **kwargs: Any) -> dict[str, Any]: + """Build query parameters, filtering None values.""" + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + return {to_camel(k): v for k, v in kwargs.items() if v is not None} + + def _build_payload(self, **kwargs: Any) -> dict[str, Any]: + """Build request payload, filtering None values.""" + from datetime import datetime + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if v is None: + continue + if isinstance(v, datetime): + result[to_camel(k)] = v.isoformat() + else: + result[to_camel(k)] = v + return result + + def list_ad_campaigns( + self, + *, + page: int | None = 1, + limit: int | None = 20, + source: str | None = "zernio", + platform: str | None = None, + status: str | None = None, + ad_account_id: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + ) -> dict[str, Any]: + """List campaigns with aggregate metrics""" + params = self._build_params( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return self._client._get("/v1/ads/campaigns", params=params) + + def update_ad_campaign_status( + self, campaign_id: str, status: str, platform: str + ) -> dict[str, Any]: + """Pause or resume a campaign""" + payload = self._build_payload( + status=status, + platform=platform, + ) + return self._client._put( + f"/v1/ads/campaigns/{campaign_id}/status", data=payload + ) + + def get_ad_tree( + self, + *, + page: int | None = 1, + limit: int | None = 20, + source: str | None = "zernio", + platform: str | None = None, + status: str | None = None, + ad_account_id: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + ) -> dict[str, Any]: + """Get nested campaign/ad-set/ad tree""" + params = self._build_params( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return self._client._get("/v1/ads/tree", params=params) + + async def alist_ad_campaigns( + self, + *, + page: int | None = 1, + limit: int | None = 20, + source: str | None = "zernio", + platform: str | None = None, + status: str | None = None, + ad_account_id: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + ) -> dict[str, Any]: + """List campaigns with aggregate metrics (async)""" + params = self._build_params( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return await self._client._aget("/v1/ads/campaigns", params=params) + + async def aupdate_ad_campaign_status( + self, campaign_id: str, status: str, platform: str + ) -> dict[str, Any]: + """Pause or resume a campaign (async)""" + payload = self._build_payload( + status=status, + platform=platform, + ) + return await self._client._aput( + f"/v1/ads/campaigns/{campaign_id}/status", data=payload + ) + + async def aget_ad_tree( + self, + *, + page: int | None = 1, + limit: int | None = 20, + source: str | None = "zernio", + platform: str | None = None, + status: str | None = None, + ad_account_id: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + ) -> dict[str, Any]: + """Get nested campaign/ad-set/ad tree (async)""" + params = self._build_params( + page=page, + limit=limit, + source=source, + platform=platform, + status=status, + ad_account_id=ad_account_id, + account_id=account_id, + profile_id=profile_id, + ) + return await self._client._aget("/v1/ads/tree", params=params) diff --git a/src/late/resources/_generated/ads.py b/src/late/resources/_generated/ads.py new file mode 100644 index 0000000..8cd1991 --- /dev/null +++ b/src/late/resources/_generated/ads.py @@ -0,0 +1,397 @@ +""" +Auto-generated ads resource. + +DO NOT EDIT THIS FILE MANUALLY. +Run `python scripts/generate_resources.py` to regenerate. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from datetime import datetime + + from ..client.base import BaseClient + + +class AdsResource: + """ + ads operations. + """ + + def __init__(self, client: BaseClient) -> None: + self._client = client + + def _build_params(self, **kwargs: Any) -> dict[str, Any]: + """Build query parameters, filtering None values.""" + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + return {to_camel(k): v for k, v in kwargs.items() if v is not None} + + def _build_payload(self, **kwargs: Any) -> dict[str, Any]: + """Build request payload, filtering None values.""" + from datetime import datetime + + def to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if v is None: + continue + if isinstance(v, datetime): + result[to_camel(k)] = v.isoformat() + else: + result[to_camel(k)] = v + return result + + def list_ads( + self, + *, + page: int | None = 1, + limit: int | None = 50, + source: str | None = "zernio", + status: str | None = None, + platform: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + campaign_id: str | None = None, + ) -> dict[str, Any]: + """List ads""" + params = self._build_params( + page=page, + limit=limit, + source=source, + status=status, + platform=platform, + account_id=account_id, + profile_id=profile_id, + campaign_id=campaign_id, + ) + return self._client._get("/v1/ads", params=params) + + def get_ad(self, ad_id: str) -> dict[str, Any]: + """Get ad details""" + return self._client._get(f"/v1/ads/{ad_id}") + + def update_ad( + self, + ad_id: str, + *, + status: str | None = None, + budget: dict[str, Any] | None = None, + targeting: dict[str, Any] | None = None, + name: str | None = None, + ) -> dict[str, Any]: + """Update ad (pause/resume, budget, targeting, name)""" + payload = self._build_payload( + status=status, + budget=budget, + targeting=targeting, + name=name, + ) + return self._client._put(f"/v1/ads/{ad_id}", data=payload) + + def delete_ad(self, ad_id: str) -> dict[str, Any]: + """Cancel an ad""" + return self._client._delete(f"/v1/ads/{ad_id}") + + def get_ad_analytics( + self, ad_id: str, *, breakdowns: str | None = None + ) -> dict[str, Any]: + """Get ad analytics with daily breakdown""" + params = self._build_params( + breakdowns=breakdowns, + ) + return self._client._get(f"/v1/ads/{ad_id}/analytics", params=params) + + def list_ad_accounts(self, account_id: str) -> dict[str, Any]: + """List ad accounts for a social account""" + params = self._build_params( + account_id=account_id, + ) + return self._client._get("/v1/ads/accounts", params=params) + + def boost_post( + self, + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget: dict[str, Any], + *, + post_id: str | None = None, + platform_post_id: str | None = None, + currency: str | None = None, + schedule: dict[str, Any] | None = None, + targeting: dict[str, Any] | None = None, + bid_amount: float | None = None, + tracking: dict[str, Any] | None = None, + special_ad_categories: list[str] | None = None, + ) -> dict[str, Any]: + """Boost an existing post as a paid ad""" + payload = self._build_payload( + post_id=post_id, + platform_post_id=platform_post_id, + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget=budget, + currency=currency, + schedule=schedule, + targeting=targeting, + bid_amount=bid_amount, + tracking=tracking, + special_ad_categories=special_ad_categories, + ) + return self._client._post("/v1/ads/boost", data=payload) + + def create_standalone_ad( + self, + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget_amount: float, + budget_type: str, + body: str, + *, + currency: str | None = None, + headline: str | None = None, + long_headline: str | None = None, + call_to_action: str | None = None, + link_url: str | None = None, + image_url: str | None = None, + business_name: str | None = None, + board_id: str | None = None, + countries: list[str] | None = None, + age_min: int | None = None, + age_max: int | None = None, + interests: list[str] | None = None, + end_date: datetime | str | None = None, + audience_id: str | None = None, + campaign_type: str | None = "display", + keywords: list[str] | None = None, + additional_headlines: list[str] | None = None, + additional_descriptions: list[str] | None = None, + ) -> dict[str, Any]: + """Create a standalone ad with custom creative""" + payload = self._build_payload( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget_amount=budget_amount, + budget_type=budget_type, + currency=currency, + headline=headline, + long_headline=long_headline, + body=body, + call_to_action=call_to_action, + link_url=link_url, + image_url=image_url, + business_name=business_name, + board_id=board_id, + countries=countries, + age_min=age_min, + age_max=age_max, + interests=interests, + end_date=end_date, + audience_id=audience_id, + campaign_type=campaign_type, + keywords=keywords, + additional_headlines=additional_headlines, + additional_descriptions=additional_descriptions, + ) + return self._client._post("/v1/ads/create", data=payload) + + def sync_external_ads(self) -> dict[str, Any]: + """Sync external ads from platform ad managers""" + return self._client._post("/v1/ads/sync") + + def search_ad_interests(self, q: str, account_id: str) -> dict[str, Any]: + """Search targeting interests""" + params = self._build_params( + q=q, + account_id=account_id, + ) + return self._client._get("/v1/ads/interests", params=params) + + async def alist_ads( + self, + *, + page: int | None = 1, + limit: int | None = 50, + source: str | None = "zernio", + status: str | None = None, + platform: str | None = None, + account_id: str | None = None, + profile_id: str | None = None, + campaign_id: str | None = None, + ) -> dict[str, Any]: + """List ads (async)""" + params = self._build_params( + page=page, + limit=limit, + source=source, + status=status, + platform=platform, + account_id=account_id, + profile_id=profile_id, + campaign_id=campaign_id, + ) + return await self._client._aget("/v1/ads", params=params) + + async def aget_ad(self, ad_id: str) -> dict[str, Any]: + """Get ad details (async)""" + return await self._client._aget(f"/v1/ads/{ad_id}") + + async def aupdate_ad( + self, + ad_id: str, + *, + status: str | None = None, + budget: dict[str, Any] | None = None, + targeting: dict[str, Any] | None = None, + name: str | None = None, + ) -> dict[str, Any]: + """Update ad (pause/resume, budget, targeting, name) (async)""" + payload = self._build_payload( + status=status, + budget=budget, + targeting=targeting, + name=name, + ) + return await self._client._aput(f"/v1/ads/{ad_id}", data=payload) + + async def adelete_ad(self, ad_id: str) -> dict[str, Any]: + """Cancel an ad (async)""" + return await self._client._adelete(f"/v1/ads/{ad_id}") + + async def aget_ad_analytics( + self, ad_id: str, *, breakdowns: str | None = None + ) -> dict[str, Any]: + """Get ad analytics with daily breakdown (async)""" + params = self._build_params( + breakdowns=breakdowns, + ) + return await self._client._aget(f"/v1/ads/{ad_id}/analytics", params=params) + + async def alist_ad_accounts(self, account_id: str) -> dict[str, Any]: + """List ad accounts for a social account (async)""" + params = self._build_params( + account_id=account_id, + ) + return await self._client._aget("/v1/ads/accounts", params=params) + + async def aboost_post( + self, + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget: dict[str, Any], + *, + post_id: str | None = None, + platform_post_id: str | None = None, + currency: str | None = None, + schedule: dict[str, Any] | None = None, + targeting: dict[str, Any] | None = None, + bid_amount: float | None = None, + tracking: dict[str, Any] | None = None, + special_ad_categories: list[str] | None = None, + ) -> dict[str, Any]: + """Boost an existing post as a paid ad (async)""" + payload = self._build_payload( + post_id=post_id, + platform_post_id=platform_post_id, + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget=budget, + currency=currency, + schedule=schedule, + targeting=targeting, + bid_amount=bid_amount, + tracking=tracking, + special_ad_categories=special_ad_categories, + ) + return await self._client._apost("/v1/ads/boost", data=payload) + + async def acreate_standalone_ad( + self, + account_id: str, + ad_account_id: str, + name: str, + goal: str, + budget_amount: float, + budget_type: str, + body: str, + *, + currency: str | None = None, + headline: str | None = None, + long_headline: str | None = None, + call_to_action: str | None = None, + link_url: str | None = None, + image_url: str | None = None, + business_name: str | None = None, + board_id: str | None = None, + countries: list[str] | None = None, + age_min: int | None = None, + age_max: int | None = None, + interests: list[str] | None = None, + end_date: datetime | str | None = None, + audience_id: str | None = None, + campaign_type: str | None = "display", + keywords: list[str] | None = None, + additional_headlines: list[str] | None = None, + additional_descriptions: list[str] | None = None, + ) -> dict[str, Any]: + """Create a standalone ad with custom creative (async)""" + payload = self._build_payload( + account_id=account_id, + ad_account_id=ad_account_id, + name=name, + goal=goal, + budget_amount=budget_amount, + budget_type=budget_type, + currency=currency, + headline=headline, + long_headline=long_headline, + body=body, + call_to_action=call_to_action, + link_url=link_url, + image_url=image_url, + business_name=business_name, + board_id=board_id, + countries=countries, + age_min=age_min, + age_max=age_max, + interests=interests, + end_date=end_date, + audience_id=audience_id, + campaign_type=campaign_type, + keywords=keywords, + additional_headlines=additional_headlines, + additional_descriptions=additional_descriptions, + ) + return await self._client._apost("/v1/ads/create", data=payload) + + async def async_external_ads(self) -> dict[str, Any]: + """Sync external ads from platform ad managers (async)""" + return await self._client._apost("/v1/ads/sync") + + async def asearch_ad_interests(self, q: str, account_id: str) -> dict[str, Any]: + """Search targeting interests (async)""" + params = self._build_params( + q=q, + account_id=account_id, + ) + return await self._client._aget("/v1/ads/interests", params=params) diff --git a/src/late/resources/_generated/posts.py b/src/late/resources/_generated/posts.py index c2bb3dc..638c8ab 100644 --- a/src/late/resources/_generated/posts.py +++ b/src/late/resources/_generated/posts.py @@ -180,6 +180,10 @@ def update_post_metadata( tags: list[str] | None = None, category_id: str | None = None, privacy_status: str | None = None, + thumbnail_url: str | None = None, + made_for_kids: bool | None = None, + contains_synthetic_media: bool | None = None, + playlist_id: str | None = None, ) -> dict[str, Any]: """Update post metadata""" payload = self._build_payload( @@ -191,6 +195,10 @@ def update_post_metadata( tags=tags, category_id=category_id, privacy_status=privacy_status, + thumbnail_url=thumbnail_url, + made_for_kids=made_for_kids, + contains_synthetic_media=contains_synthetic_media, + playlist_id=playlist_id, ) return self._client._post(f"/v1/posts/{post_id}/update-metadata", data=payload) @@ -326,6 +334,10 @@ async def aupdate_post_metadata( tags: list[str] | None = None, category_id: str | None = None, privacy_status: str | None = None, + thumbnail_url: str | None = None, + made_for_kids: bool | None = None, + contains_synthetic_media: bool | None = None, + playlist_id: str | None = None, ) -> dict[str, Any]: """Update post metadata (async)""" payload = self._build_payload( @@ -337,6 +349,10 @@ async def aupdate_post_metadata( tags=tags, category_id=category_id, privacy_status=privacy_status, + thumbnail_url=thumbnail_url, + made_for_kids=made_for_kids, + contains_synthetic_media=contains_synthetic_media, + playlist_id=playlist_id, ) return await self._client._apost( f"/v1/posts/{post_id}/update-metadata", data=payload diff --git a/src/late/resources/_generated/whatsapp.py b/src/late/resources/_generated/whatsapp.py index bcaf229..a962426 100644 --- a/src/late/resources/_generated/whatsapp.py +++ b/src/late/resources/_generated/whatsapp.py @@ -230,7 +230,7 @@ def create_whats_app_template( category: str, language: str, *, - components: list[dict[str, Any]] | None = None, + components: list[Any] | None = None, library_template_name: str | None = None, library_template_body_inputs: dict[str, Any] | None = None, library_template_button_inputs: list[dict[str, Any]] | None = None, @@ -260,7 +260,7 @@ def get_whats_app_template( ) def update_whats_app_template( - self, template_name: str, account_id: str, components: list[dict[str, Any]] + self, template_name: str, account_id: str, components: list[Any] ) -> dict[str, Any]: """Update template""" payload = self._build_payload( @@ -752,7 +752,7 @@ async def acreate_whats_app_template( category: str, language: str, *, - components: list[dict[str, Any]] | None = None, + components: list[Any] | None = None, library_template_name: str | None = None, library_template_body_inputs: dict[str, Any] | None = None, library_template_button_inputs: list[dict[str, Any]] | None = None, @@ -782,7 +782,7 @@ async def aget_whats_app_template( ) async def aupdate_whats_app_template( - self, template_name: str, account_id: str, components: list[dict[str, Any]] + self, template_name: str, account_id: str, components: list[Any] ) -> dict[str, Any]: """Update template (async)""" payload = self._build_payload( From baefe89526705f7c456c52a8782f740d7769d02f Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:17:24 +0200 Subject: [PATCH 15/17] fix: auto-wire all generated resources into client and __init__ The generation script created resource files (like ValidateResource) but never updated resources/__init__.py or late_client.py, requiring manual wiring that was easy to miss. This caused client.validate to be missing, breaking validate_subreddit MCP tool calls. Now generate_resources.py auto-updates both files: - resources/__init__.py: prefers manual implementations over auto-generated - late_client.py: imports + registration between marker comments Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/generate_mcp_tools.py | 1 + scripts/generate_resources.py | 116 +++- src/late/client/late_client.py | 61 +- src/late/resources/__init__.py | 50 +- .../resources/_generated/account_groups.py | 24 +- .../resources/_generated/account_settings.py | 64 +- src/late/resources/_generated/accounts.py | 416 ++----------- src/late/resources/_generated/ad_audiences.py | 62 +- src/late/resources/_generated/ad_campaigns.py | 72 +-- src/late/resources/_generated/ads.py | 154 +---- src/late/resources/_generated/analytics.py | 266 +------- src/late/resources/_generated/api_keys.py | 24 +- src/late/resources/_generated/broadcasts.py | 118 +--- .../_generated/comment_automations.py | 100 +-- src/late/resources/_generated/comments.py | 180 +----- src/late/resources/_generated/connect.py | 410 +++---------- src/late/resources/_generated/contacts.py | 104 +--- .../resources/_generated/custom_fields.py | 64 +- src/late/resources/_generated/invites.py | 12 +- src/late/resources/_generated/logs.py | 54 +- src/late/resources/_generated/media.py | 12 +- src/late/resources/_generated/messages.py | 218 ++----- src/late/resources/_generated/posts.py | 144 +---- src/late/resources/_generated/profiles.py | 40 +- src/late/resources/_generated/queue.py | 78 +-- src/late/resources/_generated/reddit.py | 50 +- src/late/resources/_generated/reviews.py | 54 +- src/late/resources/_generated/sequences.py | 102 +-- .../_generated/twitter_engagement.py | 12 +- src/late/resources/_generated/usage.py | 4 - src/late/resources/_generated/users.py | 4 - src/late/resources/_generated/validate.py | 22 +- src/late/resources/_generated/webhooks.py | 68 +- src/late/resources/_generated/whatsapp.py | 580 +++--------------- .../_generated/whatsapp_phone_numbers.py | 24 +- 35 files changed, 678 insertions(+), 3086 deletions(-) diff --git a/scripts/generate_mcp_tools.py b/scripts/generate_mcp_tools.py index 9cc82bb..abfb297 100644 --- a/scripts/generate_mcp_tools.py +++ b/scripts/generate_mcp_tools.py @@ -46,6 +46,7 @@ "GMB Attributes": "accounts", "GMB Place Actions": "accounts", "LinkedIn Mentions": "accounts", + "Validate": "validate", } # Operations to SKIP (not useful for MCP) diff --git a/scripts/generate_resources.py b/scripts/generate_resources.py index e093a2a..6cbea26 100644 --- a/scripts/generate_resources.py +++ b/scripts/generate_resources.py @@ -47,6 +47,7 @@ "GMB Attributes": "accounts", # Group under accounts "GMB Place Actions": "accounts", # Group under accounts "LinkedIn Mentions": "accounts", # Group under accounts + "Validate": "validate", } # Resource descriptions for docstrings @@ -67,6 +68,7 @@ "connect": "OAuth connection flows", "reddit": "Reddit search and feed", "invites": "Team invitations", + "validate": "Content and subreddit validation tools", } @@ -414,28 +416,36 @@ def generate_resource_class( return "\n".join(lines) + "\n" -def generate_init_file(resources: list[str]) -> str: - """Generate __init__.py for resources.""" +def generate_init_file(generated_resources: list[str], manual_resources: list[str]) -> str: + """Generate resources/__init__.py, preferring manual implementations over auto-generated. + + For resources that have a manual .py file in resources/ (e.g. posts.py, accounts.py), + import from the manual file. For all others, import from _generated/. + """ lines = [ '"""', - "Auto-generated resource exports.", + "API resources (auto-updated by generate_resources.py).", "", - "DO NOT EDIT THIS FILE MANUALLY.", - "Run `python scripts/generate_resources.py` to regenerate.", + "Manual resources with Pydantic validation live in this directory.", + "Auto-generated resources live in _generated/.", + "This file is regenerated automatically; do not edit by hand.", '"""', "", - "from __future__ import annotations", - "", ] - # Import each resource - for resource in sorted(resources): + all_resources = sorted(set(generated_resources) | set(manual_resources)) + + # Imports: prefer manual over auto-generated + for resource in all_resources: class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - lines.append(f"from ._generated.{resource} import {class_name}") + if resource in manual_resources: + lines.append(f"from .{resource} import {class_name}") + else: + lines.append(f"from ._generated.{resource} import {class_name}") lines.append("") lines.append("__all__ = [") - for resource in sorted(resources): + for resource in all_resources: class_name = "".join(word.title() for word in resource.split("_")) + "Resource" lines.append(f' "{class_name}",') lines.append("]") @@ -443,14 +453,50 @@ def generate_init_file(resources: list[str]) -> str: return "\n".join(lines) + "\n" -def generate_client_resources(resources: list[str]) -> str: - """Generate the resource initialization code for the client.""" - lines = [] - for resource in sorted(resources): +def update_client_file(client_path: Path, all_resources: list[str]) -> None: + """Auto-update late_client.py imports and resource registration. + + Rewrites the import block from ..resources and the resource init lines + between marker comments, preserving the rest of the file. + """ + content = client_path.read_text() + + # Build new import block + class_names = [] + for resource in sorted(all_resources): class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - attr_name = resource - lines.append(f" self.{attr_name} = {class_name}(self)") - return "\n".join(lines) + class_names.append(class_name) + + import_block = "from ..resources import (\n" + for cn in class_names: + import_block += f" {cn},\n" + import_block += ")" + + # Replace the existing import block: from ..resources import (...) + content = re.sub( + r"from \.\.resources import \(\n(?:.*?\n)*?\)", + import_block, + content, + ) + + # Build new resource registration block + reg_lines = [] + for resource in sorted(all_resources): + class_name = "".join(word.title() for word in resource.split("_")) + "Resource" + reg_lines.append(f" self.{resource} = {class_name}(self)") + reg_block = "\n".join(reg_lines) + + # Replace everything between the markers + content = re.sub( + r"( +# --- auto-registered resources \(do not edit\) ---\n)" + r".*?" + r"( +# --- end auto-registered resources ---)", + r"\1" + reg_block + r"\n\2", + content, + flags=re.DOTALL, + ) + + client_path.write_text(content) def main() -> int: @@ -495,11 +541,21 @@ def main() -> int: "params": extract_parameters(operation), }) - # Create output directory - output_dir = project_root / "src" / "late" / "resources" / "_generated" + # Paths + resources_dir = project_root / "src" / "late" / "resources" + output_dir = resources_dir / "_generated" output_dir.mkdir(parents=True, exist_ok=True) + client_path = project_root / "src" / "late" / "client" / "late_client.py" + + # Detect manual resource files (hand-written implementations that override + # auto-generated ones, e.g. posts.py, accounts.py) + manual_resources = [ + p.stem + for p in resources_dir.glob("*.py") + if p.stem not in ("__init__", "base") + ] - # Generate each resource file + # Generate each auto-generated resource file generated_resources = [] for resource_name, operations in resources.items(): code = generate_resource_class(resource_name, operations) @@ -508,7 +564,7 @@ def main() -> int: generated_resources.append(resource_name) print(f"Generated {output_file.name} with {len(operations)} methods") - # Generate __init__.py for _generated + # Generate _generated/__init__.py init_content = '"""Auto-generated resources."""\n\nfrom __future__ import annotations\n\n' for resource in sorted(generated_resources): class_name = "".join(word.title() for word in resource.split("_")) + "Resource" @@ -518,15 +574,19 @@ def main() -> int: class_name = "".join(word.title() for word in resource.split("_")) + "Resource" init_content += f' "{class_name}",\n' init_content += "]\n" - (output_dir / "__init__.py").write_text(init_content) - print(f"\nGenerated {len(generated_resources)} resource classes") - print(f"Output: {output_dir}") + # Auto-update resources/__init__.py (prefers manual over auto-generated) + parent_init = generate_init_file(generated_resources, manual_resources) + (resources_dir / "__init__.py").write_text(parent_init) + print(f"\nUpdated resources/__init__.py ({len(set(generated_resources) | set(manual_resources))} resources)") + + # Auto-update late_client.py imports and resource registration + all_resources = sorted(set(generated_resources) | set(manual_resources)) + update_client_file(client_path, all_resources) + print(f"Updated late_client.py ({len(all_resources)} resources)") - # Print resource initialization code for the client - print("\n# Add to Late client __init__:") - print(generate_client_resources(generated_resources)) + print(f"\nGenerated {len(generated_resources)} resource classes in {output_dir}") return 0 diff --git a/src/late/client/late_client.py b/src/late/client/late_client.py index bd2a892..c33a08e 100644 --- a/src/late/client/late_client.py +++ b/src/late/client/late_client.py @@ -10,12 +10,19 @@ from ..resources import ( AccountGroupsResource, + AccountSettingsResource, AccountsResource, + AdAudiencesResource, + AdCampaignsResource, + AdsResource, AnalyticsResource, ApiKeysResource, + BroadcastsResource, + CommentAutomationsResource, CommentsResource, ConnectResource, - InboxResource, + ContactsResource, + CustomFieldsResource, InvitesResource, LogsResource, MediaResource, @@ -25,10 +32,15 @@ QueueResource, RedditResource, ReviewsResource, + SequencesResource, ToolsResource, + TwitterEngagementResource, UsageResource, UsersResource, + ValidateResource, WebhooksResource, + WhatsappResource, + WhatsappPhoneNumbersResource, ) from .base import BaseClient @@ -100,31 +112,40 @@ def __init__( resolved_key, base_url=base_url, timeout=timeout, max_retries=max_retries ) - # Core resources (manual with Pydantic validation) - self.posts = PostsResource(self) - self.profiles = ProfilesResource(self) + # --- auto-registered resources (do not edit) --- + self.account_groups = AccountGroupsResource(self) + self.account_settings = AccountSettingsResource(self) self.accounts = AccountsResource(self) - self.users = UsersResource(self) - self.media = MediaResource(self) + self.ad_audiences = AdAudiencesResource(self) + self.ad_campaigns = AdCampaignsResource(self) + self.ads = AdsResource(self) self.analytics = AnalyticsResource(self) - self.tools = ToolsResource(self) - self.queue = QueueResource(self) - - # Inbox resources (comments, DMs, reviews) - self.inbox = InboxResource(self) + self.api_keys = ApiKeysResource(self) + self.broadcasts = BroadcastsResource(self) + self.comment_automations = CommentAutomationsResource(self) self.comments = CommentsResource(self) - self.messages = MessagesResource(self) - self.reviews = ReviewsResource(self) - - # Additional resources (auto-generated) - self.webhooks = WebhooksResource(self) - self.logs = LogsResource(self) self.connect = ConnectResource(self) + self.contacts = ContactsResource(self) + self.custom_fields = CustomFieldsResource(self) self.invites = InvitesResource(self) - self.api_keys = ApiKeysResource(self) - self.account_groups = AccountGroupsResource(self) - self.usage = UsageResource(self) + self.logs = LogsResource(self) + self.media = MediaResource(self) + self.messages = MessagesResource(self) + self.posts = PostsResource(self) + self.profiles = ProfilesResource(self) + self.queue = QueueResource(self) self.reddit = RedditResource(self) + self.reviews = ReviewsResource(self) + self.sequences = SequencesResource(self) + self.tools = ToolsResource(self) + self.twitter_engagement = TwitterEngagementResource(self) + self.usage = UsageResource(self) + self.users = UsersResource(self) + self.validate = ValidateResource(self) + self.webhooks = WebhooksResource(self) + self.whatsapp = WhatsappResource(self) + self.whatsapp_phone_numbers = WhatsappPhoneNumbersResource(self) + # --- end auto-registered resources --- async def __aenter__(self) -> Zernio: """Async context manager entry.""" diff --git a/src/late/resources/__init__.py b/src/late/resources/__init__.py index 4d1c92f..f9a1dc2 100644 --- a/src/late/resources/__init__.py +++ b/src/late/resources/__init__.py @@ -1,38 +1,59 @@ -"""Late API resources. +""" +API resources (auto-updated by generate_resources.py). -Manual resources with Pydantic validation are used for the main resources. -Auto-generated resources are used for additional endpoints. +Manual resources with Pydantic validation live in this directory. +Auto-generated resources live in _generated/. +This file is regenerated automatically; do not edit by hand. """ from ._generated.account_groups import AccountGroupsResource +from ._generated.account_settings import AccountSettingsResource +from .accounts import AccountsResource +from ._generated.ad_audiences import AdAudiencesResource +from ._generated.ad_campaigns import AdCampaignsResource +from ._generated.ads import AdsResource +from .analytics import AnalyticsResource from ._generated.api_keys import ApiKeysResource +from ._generated.broadcasts import BroadcastsResource +from ._generated.comment_automations import CommentAutomationsResource from ._generated.comments import CommentsResource from ._generated.connect import ConnectResource -from ._generated.inbox import InboxResource +from ._generated.contacts import ContactsResource +from ._generated.custom_fields import CustomFieldsResource from ._generated.invites import InvitesResource from ._generated.logs import LogsResource -from ._generated.messages import MessagesResource -from ._generated.reddit import RedditResource -from ._generated.reviews import ReviewsResource -from ._generated.usage import UsageResource -from ._generated.webhooks import WebhooksResource -from .accounts import AccountsResource -from .analytics import AnalyticsResource from .media import MediaResource +from ._generated.messages import MessagesResource from .posts import PostsResource from .profiles import ProfilesResource from .queue import QueueResource +from ._generated.reddit import RedditResource +from ._generated.reviews import ReviewsResource +from ._generated.sequences import SequencesResource from .tools import ToolsResource +from ._generated.twitter_engagement import TwitterEngagementResource +from ._generated.usage import UsageResource from .users import UsersResource +from ._generated.validate import ValidateResource +from ._generated.webhooks import WebhooksResource +from ._generated.whatsapp import WhatsappResource +from ._generated.whatsapp_phone_numbers import WhatsappPhoneNumbersResource __all__ = [ "AccountGroupsResource", + "AccountSettingsResource", "AccountsResource", + "AdAudiencesResource", + "AdCampaignsResource", + "AdsResource", "AnalyticsResource", "ApiKeysResource", + "BroadcastsResource", + "CommentAutomationsResource", "CommentsResource", "ConnectResource", - "InboxResource", + "ContactsResource", + "CustomFieldsResource", "InvitesResource", "LogsResource", "MediaResource", @@ -42,8 +63,13 @@ "QueueResource", "RedditResource", "ReviewsResource", + "SequencesResource", "ToolsResource", + "TwitterEngagementResource", "UsageResource", "UsersResource", + "ValidateResource", "WebhooksResource", + "WhatsappResource", + "WhatsappPhoneNumbersResource", ] diff --git a/src/late/resources/_generated/account_groups.py b/src/late/resources/_generated/account_groups.py index 6af52f6..fbaa202 100644 --- a/src/late/resources/_generated/account_groups.py +++ b/src/late/resources/_generated/account_groups.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -60,13 +56,7 @@ def create_account_group(self, name: str, account_ids: list[str]) -> dict[str, A ) return self._client._post("/v1/account-groups", data=payload) - def update_account_group( - self, - group_id: str, - *, - name: str | None = None, - account_ids: list[str] | None = None, - ) -> dict[str, Any]: + def update_account_group(self, group_id: str, *, name: str | None = None, account_ids: list[str] | None = None) -> dict[str, Any]: """Update group""" payload = self._build_payload( name=name, @@ -82,9 +72,7 @@ async def alist_account_groups(self) -> dict[str, Any]: """List groups (async)""" return await self._client._aget("/v1/account-groups") - async def acreate_account_group( - self, name: str, account_ids: list[str] - ) -> dict[str, Any]: + async def acreate_account_group(self, name: str, account_ids: list[str]) -> dict[str, Any]: """Create group (async)""" payload = self._build_payload( name=name, @@ -92,13 +80,7 @@ async def acreate_account_group( ) return await self._client._apost("/v1/account-groups", data=payload) - async def aupdate_account_group( - self, - group_id: str, - *, - name: str | None = None, - account_ids: list[str] | None = None, - ) -> dict[str, Any]: + async def aupdate_account_group(self, group_id: str, *, name: str | None = None, account_ids: list[str] | None = None) -> dict[str, Any]: """Update group (async)""" payload = self._build_payload( name=name, diff --git a/src/late/resources/_generated/account_settings.py b/src/late/resources/_generated/account_settings.py index 7db320a..304aecb 100644 --- a/src/late/resources/_generated/account_settings.py +++ b/src/late/resources/_generated/account_settings.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -52,16 +48,12 @@ def get_messenger_menu(self, account_id: str) -> dict[str, Any]: """Get FB persistent menu""" return self._client._get(f"/v1/accounts/{account_id}/messenger-menu") - def set_messenger_menu( - self, account_id: str, persistent_menu: list[dict[str, Any]] - ) -> dict[str, Any]: + def set_messenger_menu(self, account_id: str, persistent_menu: list[dict[str, Any]]) -> dict[str, Any]: """Set FB persistent menu""" payload = self._build_payload( persistent_menu=persistent_menu, ) - return self._client._put( - f"/v1/accounts/{account_id}/messenger-menu", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/messenger-menu", data=payload) def delete_messenger_menu(self, account_id: str) -> dict[str, Any]: """Delete FB persistent menu""" @@ -71,16 +63,12 @@ def get_instagram_ice_breakers(self, account_id: str) -> dict[str, Any]: """Get IG ice breakers""" return self._client._get(f"/v1/accounts/{account_id}/instagram-ice-breakers") - def set_instagram_ice_breakers( - self, account_id: str, ice_breakers: list[dict[str, Any]] - ) -> dict[str, Any]: + def set_instagram_ice_breakers(self, account_id: str, ice_breakers: list[dict[str, Any]]) -> dict[str, Any]: """Set IG ice breakers""" payload = self._build_payload( ice_breakers=ice_breakers, ) - return self._client._put( - f"/v1/accounts/{account_id}/instagram-ice-breakers", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/instagram-ice-breakers", data=payload) def delete_instagram_ice_breakers(self, account_id: str) -> dict[str, Any]: """Delete IG ice breakers""" @@ -90,16 +78,12 @@ def get_telegram_commands(self, account_id: str) -> dict[str, Any]: """Get TG bot commands""" return self._client._get(f"/v1/accounts/{account_id}/telegram-commands") - def set_telegram_commands( - self, account_id: str, commands: list[dict[str, Any]] - ) -> dict[str, Any]: + def set_telegram_commands(self, account_id: str, commands: list[dict[str, Any]]) -> dict[str, Any]: """Set TG bot commands""" payload = self._build_payload( commands=commands, ) - return self._client._put( - f"/v1/accounts/{account_id}/telegram-commands", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/telegram-commands", data=payload) def delete_telegram_commands(self, account_id: str) -> dict[str, Any]: """Delete TG bot commands""" @@ -109,16 +93,12 @@ async def aget_messenger_menu(self, account_id: str) -> dict[str, Any]: """Get FB persistent menu (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/messenger-menu") - async def aset_messenger_menu( - self, account_id: str, persistent_menu: list[dict[str, Any]] - ) -> dict[str, Any]: + async def aset_messenger_menu(self, account_id: str, persistent_menu: list[dict[str, Any]]) -> dict[str, Any]: """Set FB persistent menu (async)""" payload = self._build_payload( persistent_menu=persistent_menu, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/messenger-menu", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/messenger-menu", data=payload) async def adelete_messenger_menu(self, account_id: str) -> dict[str, Any]: """Delete FB persistent menu (async)""" @@ -126,44 +106,30 @@ async def adelete_messenger_menu(self, account_id: str) -> dict[str, Any]: async def aget_instagram_ice_breakers(self, account_id: str) -> dict[str, Any]: """Get IG ice breakers (async)""" - return await self._client._aget( - f"/v1/accounts/{account_id}/instagram-ice-breakers" - ) + return await self._client._aget(f"/v1/accounts/{account_id}/instagram-ice-breakers") - async def aset_instagram_ice_breakers( - self, account_id: str, ice_breakers: list[dict[str, Any]] - ) -> dict[str, Any]: + async def aset_instagram_ice_breakers(self, account_id: str, ice_breakers: list[dict[str, Any]]) -> dict[str, Any]: """Set IG ice breakers (async)""" payload = self._build_payload( ice_breakers=ice_breakers, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/instagram-ice-breakers", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/instagram-ice-breakers", data=payload) async def adelete_instagram_ice_breakers(self, account_id: str) -> dict[str, Any]: """Delete IG ice breakers (async)""" - return await self._client._adelete( - f"/v1/accounts/{account_id}/instagram-ice-breakers" - ) + return await self._client._adelete(f"/v1/accounts/{account_id}/instagram-ice-breakers") async def aget_telegram_commands(self, account_id: str) -> dict[str, Any]: """Get TG bot commands (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/telegram-commands") - async def aset_telegram_commands( - self, account_id: str, commands: list[dict[str, Any]] - ) -> dict[str, Any]: + async def aset_telegram_commands(self, account_id: str, commands: list[dict[str, Any]]) -> dict[str, Any]: """Set TG bot commands (async)""" payload = self._build_payload( commands=commands, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/telegram-commands", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/telegram-commands", data=payload) async def adelete_telegram_commands(self, account_id: str) -> dict[str, Any]: """Delete TG bot commands (async)""" - return await self._client._adelete( - f"/v1/accounts/{account_id}/telegram-commands" - ) + return await self._client._adelete(f"/v1/accounts/{account_id}/telegram-commands") diff --git a/src/late/resources/_generated/accounts.py b/src/late/resources/_generated/accounts.py index 91292c1..dc57422 100644 --- a/src/late/resources/_generated/accounts.py +++ b/src/late/resources/_generated/accounts.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,15 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_accounts( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - include_over_limit: bool | None = False, - page: int | None = None, - limit: int | None = None, - ) -> dict[str, Any]: + def list_accounts(self, *, profile_id: str | None = None, platform: str | None = None, include_over_limit: bool | None = False, page: int | None = None, limit: int | None = None) -> dict[str, Any]: """List accounts""" params = self._build_params( profile_id=profile_id, @@ -67,15 +55,7 @@ def list_accounts( ) return self._client._get("/v1/accounts", params=params) - def get_follower_stats( - self, - *, - account_ids: str | None = None, - profile_id: str | None = None, - from_date: str | None = None, - to_date: str | None = None, - granularity: str | None = "daily", - ) -> dict[str, Any]: + def get_follower_stats(self, *, account_ids: str | None = None, profile_id: str | None = None, from_date: str | None = None, to_date: str | None = None, granularity: str | None = "daily") -> dict[str, Any]: """Get follower stats""" params = self._build_params( account_ids=account_ids, @@ -86,13 +66,7 @@ def get_follower_stats( ) return self._client._get("/v1/accounts/follower-stats", params=params) - def update_account( - self, - account_id: str, - *, - username: str | None = None, - display_name: str | None = None, - ) -> dict[str, Any]: + def update_account(self, account_id: str, *, username: str | None = None, display_name: str | None = None) -> dict[str, Any]: """Update account""" payload = self._build_payload( username=username, @@ -104,13 +78,7 @@ def delete_account(self, account_id: str) -> dict[str, Any]: """Disconnect account""" return self._client._delete(f"/v1/accounts/{account_id}") - def get_all_accounts_health( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - status: str | None = None, - ) -> dict[str, Any]: + def get_all_accounts_health(self, *, profile_id: str | None = None, platform: str | None = None, status: str | None = None) -> dict[str, Any]: """Check accounts health""" params = self._build_params( profile_id=profile_id, @@ -123,93 +91,46 @@ def get_account_health(self, account_id: str) -> dict[str, Any]: """Check account health""" return self._client._get(f"/v1/accounts/{account_id}/health") - def get_tik_tok_creator_info( - self, account_id: str, *, media_type: str | None = "video" - ) -> dict[str, Any]: + def get_tik_tok_creator_info(self, account_id: str, *, media_type: str | None = "video") -> dict[str, Any]: """Get TikTok creator info""" params = self._build_params( media_type=media_type, ) - return self._client._get( - f"/v1/accounts/{account_id}/tiktok/creator-info", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/tiktok/creator-info", params=params) - def get_google_business_reviews( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 50, - page_token: str | None = None, - ) -> dict[str, Any]: + def get_google_business_reviews(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 50, page_token: str | None = None) -> dict[str, Any]: """Get reviews""" params = self._build_params( location_id=location_id, page_size=page_size, page_token=page_token, ) - return self._client._get( - f"/v1/accounts/{account_id}/gmb-reviews", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/gmb-reviews", params=params) - def get_google_business_food_menus( - self, account_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + def get_google_business_food_menus(self, account_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Get food menus""" params = self._build_params( location_id=location_id, ) - return self._client._get( - f"/v1/accounts/{account_id}/gmb-food-menus", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/gmb-food-menus", params=params) - def update_google_business_food_menus( - self, - account_id: str, - menus: list[Any], - *, - location_id: str | None = None, - update_mask: str | None = None, - ) -> dict[str, Any]: + def update_google_business_food_menus(self, account_id: str, menus: list[Any], *, location_id: str | None = None, update_mask: str | None = None) -> dict[str, Any]: """Update food menus""" payload = self._build_payload( menus=menus, update_mask=update_mask, ) - return self._client._put( - f"/v1/accounts/{account_id}/gmb-food-menus", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/gmb-food-menus", data=payload) - def get_google_business_location_details( - self, - account_id: str, - *, - location_id: str | None = None, - read_mask: str | None = None, - ) -> dict[str, Any]: + def get_google_business_location_details(self, account_id: str, *, location_id: str | None = None, read_mask: str | None = None) -> dict[str, Any]: """Get location details""" params = self._build_params( location_id=location_id, read_mask=read_mask, ) - return self._client._get( - f"/v1/accounts/{account_id}/gmb-location-details", params=params - ) - - def update_google_business_location_details( - self, - account_id: str, - update_mask: str, - *, - location_id: str | None = None, - regular_hours: dict[str, Any] | None = None, - special_hours: dict[str, Any] | None = None, - profile: dict[str, Any] | None = None, - website_uri: str | None = None, - phone_numbers: dict[str, Any] | None = None, - categories: dict[str, Any] | None = None, - service_items: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + return self._client._get(f"/v1/accounts/{account_id}/gmb-location-details", params=params) + + def update_google_business_location_details(self, account_id: str, update_mask: str, *, location_id: str | None = None, regular_hours: dict[str, Any] | None = None, special_hours: dict[str, Any] | None = None, profile: dict[str, Any] | None = None, website_uri: str | None = None, phone_numbers: dict[str, Any] | None = None, categories: dict[str, Any] | None = None, service_items: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Update location details""" payload = self._build_payload( update_mask=update_mask, @@ -221,18 +142,9 @@ def update_google_business_location_details( categories=categories, service_items=service_items, ) - return self._client._put( - f"/v1/accounts/{account_id}/gmb-location-details", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/gmb-location-details", data=payload) - def list_google_business_media( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 100, - page_token: str | None = None, - ) -> dict[str, Any]: + def list_google_business_media(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 100, page_token: str | None = None) -> dict[str, Any]: """List media""" params = self._build_params( location_id=location_id, @@ -241,16 +153,7 @@ def list_google_business_media( ) return self._client._get(f"/v1/accounts/{account_id}/gmb-media", params=params) - def create_google_business_media( - self, - account_id: str, - source_url: str, - *, - location_id: str | None = None, - media_format: str | None = "PHOTO", - description: str | None = None, - category: str | None = None, - ) -> dict[str, Any]: + def create_google_business_media(self, account_id: str, source_url: str, *, location_id: str | None = None, media_format: str | None = "PHOTO", description: str | None = None, category: str | None = None) -> dict[str, Any]: """Upload photo""" payload = self._build_payload( source_url=source_url, @@ -260,114 +163,63 @@ def create_google_business_media( ) return self._client._post(f"/v1/accounts/{account_id}/gmb-media", data=payload) - def delete_google_business_media( - self, account_id: str, media_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + def delete_google_business_media(self, account_id: str, media_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Delete photo""" params = self._build_params( location_id=location_id, media_id=media_id, ) - return self._client._delete( - f"/v1/accounts/{account_id}/gmb-media", params=params - ) + return self._client._delete(f"/v1/accounts/{account_id}/gmb-media", params=params) - def get_google_business_attributes( - self, account_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + def get_google_business_attributes(self, account_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Get attributes""" params = self._build_params( location_id=location_id, ) - return self._client._get( - f"/v1/accounts/{account_id}/gmb-attributes", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/gmb-attributes", params=params) - def update_google_business_attributes( - self, - account_id: str, - attributes: list[dict[str, Any]], - attribute_mask: str, - *, - location_id: str | None = None, - ) -> dict[str, Any]: + def update_google_business_attributes(self, account_id: str, attributes: list[dict[str, Any]], attribute_mask: str, *, location_id: str | None = None) -> dict[str, Any]: """Update attributes""" payload = self._build_payload( attributes=attributes, attribute_mask=attribute_mask, ) - return self._client._put( - f"/v1/accounts/{account_id}/gmb-attributes", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/gmb-attributes", data=payload) - def list_google_business_place_actions( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 100, - page_token: str | None = None, - ) -> dict[str, Any]: + def list_google_business_place_actions(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 100, page_token: str | None = None) -> dict[str, Any]: """List action links""" params = self._build_params( location_id=location_id, page_size=page_size, page_token=page_token, ) - return self._client._get( - f"/v1/accounts/{account_id}/gmb-place-actions", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/gmb-place-actions", params=params) - def create_google_business_place_action( - self, - account_id: str, - uri: str, - place_action_type: str, - *, - location_id: str | None = None, - ) -> dict[str, Any]: + def create_google_business_place_action(self, account_id: str, uri: str, place_action_type: str, *, location_id: str | None = None) -> dict[str, Any]: """Create action link""" payload = self._build_payload( uri=uri, place_action_type=place_action_type, ) - return self._client._post( - f"/v1/accounts/{account_id}/gmb-place-actions", data=payload - ) + return self._client._post(f"/v1/accounts/{account_id}/gmb-place-actions", data=payload) - def delete_google_business_place_action( - self, account_id: str, name: str, *, location_id: str | None = None - ) -> dict[str, Any]: + def delete_google_business_place_action(self, account_id: str, name: str, *, location_id: str | None = None) -> dict[str, Any]: """Delete action link""" params = self._build_params( location_id=location_id, name=name, ) - return self._client._delete( - f"/v1/accounts/{account_id}/gmb-place-actions", params=params - ) + return self._client._delete(f"/v1/accounts/{account_id}/gmb-place-actions", params=params) - def get_linked_in_mentions( - self, account_id: str, url: str, *, display_name: str | None = None - ) -> dict[str, Any]: + def get_linked_in_mentions(self, account_id: str, url: str, *, display_name: str | None = None) -> dict[str, Any]: """Resolve LinkedIn mention""" params = self._build_params( url=url, display_name=display_name, ) - return self._client._get( - f"/v1/accounts/{account_id}/linkedin-mentions", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/linkedin-mentions", params=params) - async def alist_accounts( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - include_over_limit: bool | None = False, - page: int | None = None, - limit: int | None = None, - ) -> dict[str, Any]: + async def alist_accounts(self, *, profile_id: str | None = None, platform: str | None = None, include_over_limit: bool | None = False, page: int | None = None, limit: int | None = None) -> dict[str, Any]: """List accounts (async)""" params = self._build_params( profile_id=profile_id, @@ -378,15 +230,7 @@ async def alist_accounts( ) return await self._client._aget("/v1/accounts", params=params) - async def aget_follower_stats( - self, - *, - account_ids: str | None = None, - profile_id: str | None = None, - from_date: str | None = None, - to_date: str | None = None, - granularity: str | None = "daily", - ) -> dict[str, Any]: + async def aget_follower_stats(self, *, account_ids: str | None = None, profile_id: str | None = None, from_date: str | None = None, to_date: str | None = None, granularity: str | None = "daily") -> dict[str, Any]: """Get follower stats (async)""" params = self._build_params( account_ids=account_ids, @@ -397,13 +241,7 @@ async def aget_follower_stats( ) return await self._client._aget("/v1/accounts/follower-stats", params=params) - async def aupdate_account( - self, - account_id: str, - *, - username: str | None = None, - display_name: str | None = None, - ) -> dict[str, Any]: + async def aupdate_account(self, account_id: str, *, username: str | None = None, display_name: str | None = None) -> dict[str, Any]: """Update account (async)""" payload = self._build_payload( username=username, @@ -415,13 +253,7 @@ async def adelete_account(self, account_id: str) -> dict[str, Any]: """Disconnect account (async)""" return await self._client._adelete(f"/v1/accounts/{account_id}") - async def aget_all_accounts_health( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - status: str | None = None, - ) -> dict[str, Any]: + async def aget_all_accounts_health(self, *, profile_id: str | None = None, platform: str | None = None, status: str | None = None) -> dict[str, Any]: """Check accounts health (async)""" params = self._build_params( profile_id=profile_id, @@ -434,93 +266,46 @@ async def aget_account_health(self, account_id: str) -> dict[str, Any]: """Check account health (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/health") - async def aget_tik_tok_creator_info( - self, account_id: str, *, media_type: str | None = "video" - ) -> dict[str, Any]: + async def aget_tik_tok_creator_info(self, account_id: str, *, media_type: str | None = "video") -> dict[str, Any]: """Get TikTok creator info (async)""" params = self._build_params( media_type=media_type, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/tiktok/creator-info", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/tiktok/creator-info", params=params) - async def aget_google_business_reviews( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 50, - page_token: str | None = None, - ) -> dict[str, Any]: + async def aget_google_business_reviews(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 50, page_token: str | None = None) -> dict[str, Any]: """Get reviews (async)""" params = self._build_params( location_id=location_id, page_size=page_size, page_token=page_token, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-reviews", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-reviews", params=params) - async def aget_google_business_food_menus( - self, account_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + async def aget_google_business_food_menus(self, account_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Get food menus (async)""" params = self._build_params( location_id=location_id, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-food-menus", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-food-menus", params=params) - async def aupdate_google_business_food_menus( - self, - account_id: str, - menus: list[Any], - *, - location_id: str | None = None, - update_mask: str | None = None, - ) -> dict[str, Any]: + async def aupdate_google_business_food_menus(self, account_id: str, menus: list[Any], *, location_id: str | None = None, update_mask: str | None = None) -> dict[str, Any]: """Update food menus (async)""" payload = self._build_payload( menus=menus, update_mask=update_mask, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/gmb-food-menus", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/gmb-food-menus", data=payload) - async def aget_google_business_location_details( - self, - account_id: str, - *, - location_id: str | None = None, - read_mask: str | None = None, - ) -> dict[str, Any]: + async def aget_google_business_location_details(self, account_id: str, *, location_id: str | None = None, read_mask: str | None = None) -> dict[str, Any]: """Get location details (async)""" params = self._build_params( location_id=location_id, read_mask=read_mask, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-location-details", params=params - ) - - async def aupdate_google_business_location_details( - self, - account_id: str, - update_mask: str, - *, - location_id: str | None = None, - regular_hours: dict[str, Any] | None = None, - special_hours: dict[str, Any] | None = None, - profile: dict[str, Any] | None = None, - website_uri: str | None = None, - phone_numbers: dict[str, Any] | None = None, - categories: dict[str, Any] | None = None, - service_items: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-location-details", params=params) + + async def aupdate_google_business_location_details(self, account_id: str, update_mask: str, *, location_id: str | None = None, regular_hours: dict[str, Any] | None = None, special_hours: dict[str, Any] | None = None, profile: dict[str, Any] | None = None, website_uri: str | None = None, phone_numbers: dict[str, Any] | None = None, categories: dict[str, Any] | None = None, service_items: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Update location details (async)""" payload = self._build_payload( update_mask=update_mask, @@ -532,38 +317,18 @@ async def aupdate_google_business_location_details( categories=categories, service_items=service_items, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/gmb-location-details", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/gmb-location-details", data=payload) - async def alist_google_business_media( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 100, - page_token: str | None = None, - ) -> dict[str, Any]: + async def alist_google_business_media(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 100, page_token: str | None = None) -> dict[str, Any]: """List media (async)""" params = self._build_params( location_id=location_id, page_size=page_size, page_token=page_token, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-media", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-media", params=params) - async def acreate_google_business_media( - self, - account_id: str, - source_url: str, - *, - location_id: str | None = None, - media_format: str | None = "PHOTO", - description: str | None = None, - category: str | None = None, - ) -> dict[str, Any]: + async def acreate_google_business_media(self, account_id: str, source_url: str, *, location_id: str | None = None, media_format: str | None = "PHOTO", description: str | None = None, category: str | None = None) -> dict[str, Any]: """Upload photo (async)""" payload = self._build_payload( source_url=source_url, @@ -571,105 +336,60 @@ async def acreate_google_business_media( description=description, category=category, ) - return await self._client._apost( - f"/v1/accounts/{account_id}/gmb-media", data=payload - ) + return await self._client._apost(f"/v1/accounts/{account_id}/gmb-media", data=payload) - async def adelete_google_business_media( - self, account_id: str, media_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + async def adelete_google_business_media(self, account_id: str, media_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Delete photo (async)""" params = self._build_params( location_id=location_id, media_id=media_id, ) - return await self._client._adelete( - f"/v1/accounts/{account_id}/gmb-media", params=params - ) + return await self._client._adelete(f"/v1/accounts/{account_id}/gmb-media", params=params) - async def aget_google_business_attributes( - self, account_id: str, *, location_id: str | None = None - ) -> dict[str, Any]: + async def aget_google_business_attributes(self, account_id: str, *, location_id: str | None = None) -> dict[str, Any]: """Get attributes (async)""" params = self._build_params( location_id=location_id, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-attributes", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-attributes", params=params) - async def aupdate_google_business_attributes( - self, - account_id: str, - attributes: list[dict[str, Any]], - attribute_mask: str, - *, - location_id: str | None = None, - ) -> dict[str, Any]: + async def aupdate_google_business_attributes(self, account_id: str, attributes: list[dict[str, Any]], attribute_mask: str, *, location_id: str | None = None) -> dict[str, Any]: """Update attributes (async)""" payload = self._build_payload( attributes=attributes, attribute_mask=attribute_mask, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/gmb-attributes", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/gmb-attributes", data=payload) - async def alist_google_business_place_actions( - self, - account_id: str, - *, - location_id: str | None = None, - page_size: int | None = 100, - page_token: str | None = None, - ) -> dict[str, Any]: + async def alist_google_business_place_actions(self, account_id: str, *, location_id: str | None = None, page_size: int | None = 100, page_token: str | None = None) -> dict[str, Any]: """List action links (async)""" params = self._build_params( location_id=location_id, page_size=page_size, page_token=page_token, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/gmb-place-actions", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/gmb-place-actions", params=params) - async def acreate_google_business_place_action( - self, - account_id: str, - uri: str, - place_action_type: str, - *, - location_id: str | None = None, - ) -> dict[str, Any]: + async def acreate_google_business_place_action(self, account_id: str, uri: str, place_action_type: str, *, location_id: str | None = None) -> dict[str, Any]: """Create action link (async)""" payload = self._build_payload( uri=uri, place_action_type=place_action_type, ) - return await self._client._apost( - f"/v1/accounts/{account_id}/gmb-place-actions", data=payload - ) + return await self._client._apost(f"/v1/accounts/{account_id}/gmb-place-actions", data=payload) - async def adelete_google_business_place_action( - self, account_id: str, name: str, *, location_id: str | None = None - ) -> dict[str, Any]: + async def adelete_google_business_place_action(self, account_id: str, name: str, *, location_id: str | None = None) -> dict[str, Any]: """Delete action link (async)""" params = self._build_params( location_id=location_id, name=name, ) - return await self._client._adelete( - f"/v1/accounts/{account_id}/gmb-place-actions", params=params - ) + return await self._client._adelete(f"/v1/accounts/{account_id}/gmb-place-actions", params=params) - async def aget_linked_in_mentions( - self, account_id: str, url: str, *, display_name: str | None = None - ) -> dict[str, Any]: + async def aget_linked_in_mentions(self, account_id: str, url: str, *, display_name: str | None = None) -> dict[str, Any]: """Resolve LinkedIn mention (async)""" params = self._build_params( url=url, display_name=display_name, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/linkedin-mentions", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/linkedin-mentions", params=params) diff --git a/src/late/resources/_generated/ad_audiences.py b/src/late/resources/_generated/ad_audiences.py index 60f1f47..f09229a 100644 --- a/src/late/resources/_generated/ad_audiences.py +++ b/src/late/resources/_generated/ad_audiences.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,9 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_ad_audiences( - self, account_id: str, ad_account_id: str, *, platform: str | None = None - ) -> dict[str, Any]: + def list_ad_audiences(self, account_id: str, ad_account_id: str, *, platform: str | None = None) -> dict[str, Any]: """List custom audiences""" params = self._build_params( account_id=account_id, @@ -59,22 +53,7 @@ def list_ad_audiences( ) return self._client._get("/v1/ads/audiences", params=params) - def create_ad_audience( - self, - account_id: str, - ad_account_id: str, - name: str, - type: str, - *, - description: str | None = None, - pixel_id: str | None = None, - retention_days: int | None = None, - source_audience_id: str | None = None, - country: str | None = None, - ratio: float | None = None, - rule: dict[str, Any] | None = None, - customer_file_source: str | None = None, - ) -> dict[str, Any]: + def create_ad_audience(self, account_id: str, ad_account_id: str, name: str, type: str, *, description: str | None = None, pixel_id: str | None = None, retention_days: int | None = None, source_audience_id: str | None = None, country: str | None = None, ratio: float | None = None, rule: dict[str, Any] | None = None, customer_file_source: str | None = None) -> dict[str, Any]: """Create a custom audience (Meta only)""" payload = self._build_payload( account_id=account_id, @@ -100,20 +79,14 @@ def delete_ad_audience(self, audience_id: str) -> dict[str, Any]: """Delete a custom audience""" return self._client._delete(f"/v1/ads/audiences/{audience_id}") - def add_users_to_ad_audience( - self, audience_id: str, users: list[dict[str, Any]] - ) -> dict[str, Any]: + def add_users_to_ad_audience(self, audience_id: str, users: list[dict[str, Any]]) -> dict[str, Any]: """Add users to a customer list audience""" payload = self._build_payload( users=users, ) - return self._client._post( - f"/v1/ads/audiences/{audience_id}/users", data=payload - ) + return self._client._post(f"/v1/ads/audiences/{audience_id}/users", data=payload) - async def alist_ad_audiences( - self, account_id: str, ad_account_id: str, *, platform: str | None = None - ) -> dict[str, Any]: + async def alist_ad_audiences(self, account_id: str, ad_account_id: str, *, platform: str | None = None) -> dict[str, Any]: """List custom audiences (async)""" params = self._build_params( account_id=account_id, @@ -122,22 +95,7 @@ async def alist_ad_audiences( ) return await self._client._aget("/v1/ads/audiences", params=params) - async def acreate_ad_audience( - self, - account_id: str, - ad_account_id: str, - name: str, - type: str, - *, - description: str | None = None, - pixel_id: str | None = None, - retention_days: int | None = None, - source_audience_id: str | None = None, - country: str | None = None, - ratio: float | None = None, - rule: dict[str, Any] | None = None, - customer_file_source: str | None = None, - ) -> dict[str, Any]: + async def acreate_ad_audience(self, account_id: str, ad_account_id: str, name: str, type: str, *, description: str | None = None, pixel_id: str | None = None, retention_days: int | None = None, source_audience_id: str | None = None, country: str | None = None, ratio: float | None = None, rule: dict[str, Any] | None = None, customer_file_source: str | None = None) -> dict[str, Any]: """Create a custom audience (Meta only) (async)""" payload = self._build_payload( account_id=account_id, @@ -163,13 +121,9 @@ async def adelete_ad_audience(self, audience_id: str) -> dict[str, Any]: """Delete a custom audience (async)""" return await self._client._adelete(f"/v1/ads/audiences/{audience_id}") - async def aadd_users_to_ad_audience( - self, audience_id: str, users: list[dict[str, Any]] - ) -> dict[str, Any]: + async def aadd_users_to_ad_audience(self, audience_id: str, users: list[dict[str, Any]]) -> dict[str, Any]: """Add users to a customer list audience (async)""" payload = self._build_payload( users=users, ) - return await self._client._apost( - f"/v1/ads/audiences/{audience_id}/users", data=payload - ) + return await self._client._apost(f"/v1/ads/audiences/{audience_id}/users", data=payload) diff --git a/src/late/resources/_generated/ad_campaigns.py b/src/late/resources/_generated/ad_campaigns.py index 88361bb..faabe1b 100644 --- a/src/late/resources/_generated/ad_campaigns.py +++ b/src/late/resources/_generated/ad_campaigns.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,18 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_ad_campaigns( - self, - *, - page: int | None = 1, - limit: int | None = 20, - source: str | None = "zernio", - platform: str | None = None, - status: str | None = None, - ad_account_id: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - ) -> dict[str, Any]: + def list_ad_campaigns(self, *, page: int | None = 1, limit: int | None = 20, source: str | None = "zernio", platform: str | None = None, status: str | None = None, ad_account_id: str | None = None, account_id: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """List campaigns with aggregate metrics""" params = self._build_params( page=page, @@ -73,30 +58,15 @@ def list_ad_campaigns( ) return self._client._get("/v1/ads/campaigns", params=params) - def update_ad_campaign_status( - self, campaign_id: str, status: str, platform: str - ) -> dict[str, Any]: + def update_ad_campaign_status(self, campaign_id: str, status: str, platform: str) -> dict[str, Any]: """Pause or resume a campaign""" payload = self._build_payload( status=status, platform=platform, ) - return self._client._put( - f"/v1/ads/campaigns/{campaign_id}/status", data=payload - ) + return self._client._put(f"/v1/ads/campaigns/{campaign_id}/status", data=payload) - def get_ad_tree( - self, - *, - page: int | None = 1, - limit: int | None = 20, - source: str | None = "zernio", - platform: str | None = None, - status: str | None = None, - ad_account_id: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - ) -> dict[str, Any]: + def get_ad_tree(self, *, page: int | None = 1, limit: int | None = 20, source: str | None = "zernio", platform: str | None = None, status: str | None = None, ad_account_id: str | None = None, account_id: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """Get nested campaign/ad-set/ad tree""" params = self._build_params( page=page, @@ -110,18 +80,7 @@ def get_ad_tree( ) return self._client._get("/v1/ads/tree", params=params) - async def alist_ad_campaigns( - self, - *, - page: int | None = 1, - limit: int | None = 20, - source: str | None = "zernio", - platform: str | None = None, - status: str | None = None, - ad_account_id: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - ) -> dict[str, Any]: + async def alist_ad_campaigns(self, *, page: int | None = 1, limit: int | None = 20, source: str | None = "zernio", platform: str | None = None, status: str | None = None, ad_account_id: str | None = None, account_id: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """List campaigns with aggregate metrics (async)""" params = self._build_params( page=page, @@ -135,30 +94,15 @@ async def alist_ad_campaigns( ) return await self._client._aget("/v1/ads/campaigns", params=params) - async def aupdate_ad_campaign_status( - self, campaign_id: str, status: str, platform: str - ) -> dict[str, Any]: + async def aupdate_ad_campaign_status(self, campaign_id: str, status: str, platform: str) -> dict[str, Any]: """Pause or resume a campaign (async)""" payload = self._build_payload( status=status, platform=platform, ) - return await self._client._aput( - f"/v1/ads/campaigns/{campaign_id}/status", data=payload - ) + return await self._client._aput(f"/v1/ads/campaigns/{campaign_id}/status", data=payload) - async def aget_ad_tree( - self, - *, - page: int | None = 1, - limit: int | None = 20, - source: str | None = "zernio", - platform: str | None = None, - status: str | None = None, - ad_account_id: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - ) -> dict[str, Any]: + async def aget_ad_tree(self, *, page: int | None = 1, limit: int | None = 20, source: str | None = "zernio", platform: str | None = None, status: str | None = None, ad_account_id: str | None = None, account_id: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """Get nested campaign/ad-set/ad tree (async)""" params = self._build_params( page=page, diff --git a/src/late/resources/_generated/ads.py b/src/late/resources/_generated/ads.py index 8cd1991..168df54 100644 --- a/src/late/resources/_generated/ads.py +++ b/src/late/resources/_generated/ads.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,18 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_ads( - self, - *, - page: int | None = 1, - limit: int | None = 50, - source: str | None = "zernio", - status: str | None = None, - platform: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - campaign_id: str | None = None, - ) -> dict[str, Any]: + def list_ads(self, *, page: int | None = 1, limit: int | None = 50, source: str | None = "zernio", status: str | None = None, platform: str | None = None, account_id: str | None = None, profile_id: str | None = None, campaign_id: str | None = None) -> dict[str, Any]: """List ads""" params = self._build_params( page=page, @@ -79,15 +64,7 @@ def get_ad(self, ad_id: str) -> dict[str, Any]: """Get ad details""" return self._client._get(f"/v1/ads/{ad_id}") - def update_ad( - self, - ad_id: str, - *, - status: str | None = None, - budget: dict[str, Any] | None = None, - targeting: dict[str, Any] | None = None, - name: str | None = None, - ) -> dict[str, Any]: + def update_ad(self, ad_id: str, *, status: str | None = None, budget: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, name: str | None = None) -> dict[str, Any]: """Update ad (pause/resume, budget, targeting, name)""" payload = self._build_payload( status=status, @@ -101,9 +78,7 @@ def delete_ad(self, ad_id: str) -> dict[str, Any]: """Cancel an ad""" return self._client._delete(f"/v1/ads/{ad_id}") - def get_ad_analytics( - self, ad_id: str, *, breakdowns: str | None = None - ) -> dict[str, Any]: + def get_ad_analytics(self, ad_id: str, *, breakdowns: str | None = None) -> dict[str, Any]: """Get ad analytics with daily breakdown""" params = self._build_params( breakdowns=breakdowns, @@ -117,23 +92,7 @@ def list_ad_accounts(self, account_id: str) -> dict[str, Any]: ) return self._client._get("/v1/ads/accounts", params=params) - def boost_post( - self, - account_id: str, - ad_account_id: str, - name: str, - goal: str, - budget: dict[str, Any], - *, - post_id: str | None = None, - platform_post_id: str | None = None, - currency: str | None = None, - schedule: dict[str, Any] | None = None, - targeting: dict[str, Any] | None = None, - bid_amount: float | None = None, - tracking: dict[str, Any] | None = None, - special_ad_categories: list[str] | None = None, - ) -> dict[str, Any]: + def boost_post(self, account_id: str, ad_account_id: str, name: str, goal: str, budget: dict[str, Any], *, post_id: str | None = None, platform_post_id: str | None = None, currency: str | None = None, schedule: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, bid_amount: float | None = None, tracking: dict[str, Any] | None = None, special_ad_categories: list[str] | None = None) -> dict[str, Any]: """Boost an existing post as a paid ad""" payload = self._build_payload( post_id=post_id, @@ -152,35 +111,7 @@ def boost_post( ) return self._client._post("/v1/ads/boost", data=payload) - def create_standalone_ad( - self, - account_id: str, - ad_account_id: str, - name: str, - goal: str, - budget_amount: float, - budget_type: str, - body: str, - *, - currency: str | None = None, - headline: str | None = None, - long_headline: str | None = None, - call_to_action: str | None = None, - link_url: str | None = None, - image_url: str | None = None, - business_name: str | None = None, - board_id: str | None = None, - countries: list[str] | None = None, - age_min: int | None = None, - age_max: int | None = None, - interests: list[str] | None = None, - end_date: datetime | str | None = None, - audience_id: str | None = None, - campaign_type: str | None = "display", - keywords: list[str] | None = None, - additional_headlines: list[str] | None = None, - additional_descriptions: list[str] | None = None, - ) -> dict[str, Any]: + def create_standalone_ad(self, account_id: str, ad_account_id: str, name: str, goal: str, budget_amount: float, budget_type: str, body: str, *, currency: str | None = None, headline: str | None = None, long_headline: str | None = None, call_to_action: str | None = None, link_url: str | None = None, image_url: str | None = None, business_name: str | None = None, board_id: str | None = None, countries: list[str] | None = None, age_min: int | None = None, age_max: int | None = None, interests: list[str] | None = None, end_date: datetime | str | None = None, audience_id: str | None = None, campaign_type: str | None = "display", keywords: list[str] | None = None, additional_headlines: list[str] | None = None, additional_descriptions: list[str] | None = None) -> dict[str, Any]: """Create a standalone ad with custom creative""" payload = self._build_payload( account_id=account_id, @@ -223,18 +154,7 @@ def search_ad_interests(self, q: str, account_id: str) -> dict[str, Any]: ) return self._client._get("/v1/ads/interests", params=params) - async def alist_ads( - self, - *, - page: int | None = 1, - limit: int | None = 50, - source: str | None = "zernio", - status: str | None = None, - platform: str | None = None, - account_id: str | None = None, - profile_id: str | None = None, - campaign_id: str | None = None, - ) -> dict[str, Any]: + async def alist_ads(self, *, page: int | None = 1, limit: int | None = 50, source: str | None = "zernio", status: str | None = None, platform: str | None = None, account_id: str | None = None, profile_id: str | None = None, campaign_id: str | None = None) -> dict[str, Any]: """List ads (async)""" params = self._build_params( page=page, @@ -252,15 +172,7 @@ async def aget_ad(self, ad_id: str) -> dict[str, Any]: """Get ad details (async)""" return await self._client._aget(f"/v1/ads/{ad_id}") - async def aupdate_ad( - self, - ad_id: str, - *, - status: str | None = None, - budget: dict[str, Any] | None = None, - targeting: dict[str, Any] | None = None, - name: str | None = None, - ) -> dict[str, Any]: + async def aupdate_ad(self, ad_id: str, *, status: str | None = None, budget: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, name: str | None = None) -> dict[str, Any]: """Update ad (pause/resume, budget, targeting, name) (async)""" payload = self._build_payload( status=status, @@ -274,9 +186,7 @@ async def adelete_ad(self, ad_id: str) -> dict[str, Any]: """Cancel an ad (async)""" return await self._client._adelete(f"/v1/ads/{ad_id}") - async def aget_ad_analytics( - self, ad_id: str, *, breakdowns: str | None = None - ) -> dict[str, Any]: + async def aget_ad_analytics(self, ad_id: str, *, breakdowns: str | None = None) -> dict[str, Any]: """Get ad analytics with daily breakdown (async)""" params = self._build_params( breakdowns=breakdowns, @@ -290,23 +200,7 @@ async def alist_ad_accounts(self, account_id: str) -> dict[str, Any]: ) return await self._client._aget("/v1/ads/accounts", params=params) - async def aboost_post( - self, - account_id: str, - ad_account_id: str, - name: str, - goal: str, - budget: dict[str, Any], - *, - post_id: str | None = None, - platform_post_id: str | None = None, - currency: str | None = None, - schedule: dict[str, Any] | None = None, - targeting: dict[str, Any] | None = None, - bid_amount: float | None = None, - tracking: dict[str, Any] | None = None, - special_ad_categories: list[str] | None = None, - ) -> dict[str, Any]: + async def aboost_post(self, account_id: str, ad_account_id: str, name: str, goal: str, budget: dict[str, Any], *, post_id: str | None = None, platform_post_id: str | None = None, currency: str | None = None, schedule: dict[str, Any] | None = None, targeting: dict[str, Any] | None = None, bid_amount: float | None = None, tracking: dict[str, Any] | None = None, special_ad_categories: list[str] | None = None) -> dict[str, Any]: """Boost an existing post as a paid ad (async)""" payload = self._build_payload( post_id=post_id, @@ -325,35 +219,7 @@ async def aboost_post( ) return await self._client._apost("/v1/ads/boost", data=payload) - async def acreate_standalone_ad( - self, - account_id: str, - ad_account_id: str, - name: str, - goal: str, - budget_amount: float, - budget_type: str, - body: str, - *, - currency: str | None = None, - headline: str | None = None, - long_headline: str | None = None, - call_to_action: str | None = None, - link_url: str | None = None, - image_url: str | None = None, - business_name: str | None = None, - board_id: str | None = None, - countries: list[str] | None = None, - age_min: int | None = None, - age_max: int | None = None, - interests: list[str] | None = None, - end_date: datetime | str | None = None, - audience_id: str | None = None, - campaign_type: str | None = "display", - keywords: list[str] | None = None, - additional_headlines: list[str] | None = None, - additional_descriptions: list[str] | None = None, - ) -> dict[str, Any]: + async def acreate_standalone_ad(self, account_id: str, ad_account_id: str, name: str, goal: str, budget_amount: float, budget_type: str, body: str, *, currency: str | None = None, headline: str | None = None, long_headline: str | None = None, call_to_action: str | None = None, link_url: str | None = None, image_url: str | None = None, business_name: str | None = None, board_id: str | None = None, countries: list[str] | None = None, age_min: int | None = None, age_max: int | None = None, interests: list[str] | None = None, end_date: datetime | str | None = None, audience_id: str | None = None, campaign_type: str | None = "display", keywords: list[str] | None = None, additional_headlines: list[str] | None = None, additional_descriptions: list[str] | None = None) -> dict[str, Any]: """Create a standalone ad with custom creative (async)""" payload = self._build_payload( account_id=account_id, diff --git a/src/late/resources/_generated/analytics.py b/src/late/resources/_generated/analytics.py index 02f2010..600f0cd 100644 --- a/src/late/resources/_generated/analytics.py +++ b/src/late/resources/_generated/analytics.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,21 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def get_analytics( - self, - *, - post_id: str | None = None, - platform: str | None = None, - profile_id: str | None = None, - account_id: str | None = None, - source: str | None = "all", - from_date: str | None = None, - to_date: str | None = None, - limit: int | None = 50, - page: int | None = 1, - sort_by: str | None = "date", - order: str | None = "desc", - ) -> dict[str, Any]: + def get_analytics(self, *, post_id: str | None = None, platform: str | None = None, profile_id: str | None = None, account_id: str | None = None, source: str | None = "all", from_date: str | None = None, to_date: str | None = None, limit: int | None = 50, page: int | None = 1, sort_by: str | None = "date", order: str | None = "desc") -> dict[str, Any]: """Get post analytics""" params = self._build_params( post_id=post_id, @@ -81,14 +63,7 @@ def get_analytics( ) return self._client._get("/v1/analytics", params=params) - def get_you_tube_daily_views( - self, - video_id: str, - account_id: str, - *, - start_date: str | None = None, - end_date: str | None = None, - ) -> dict[str, Any]: + def get_you_tube_daily_views(self, video_id: str, account_id: str, *, start_date: str | None = None, end_date: str | None = None) -> dict[str, Any]: """Get YouTube daily views""" params = self._build_params( video_id=video_id, @@ -98,16 +73,7 @@ def get_you_tube_daily_views( ) return self._client._get("/v1/analytics/youtube/daily-views", params=params) - def get_instagram_account_insights( - self, - account_id: str, - *, - metrics: str | None = None, - since: str | None = None, - until: str | None = None, - metric_type: str | None = "total_value", - breakdown: str | None = None, - ) -> dict[str, Any]: + def get_instagram_account_insights(self, account_id: str, *, metrics: str | None = None, since: str | None = None, until: str | None = None, metric_type: str | None = "total_value", breakdown: str | None = None) -> dict[str, Any]: """Get Instagram account-level insights""" params = self._build_params( account_id=account_id, @@ -117,18 +83,9 @@ def get_instagram_account_insights( metric_type=metric_type, breakdown=breakdown, ) - return self._client._get( - "/v1/analytics/instagram/account-insights", params=params - ) + return self._client._get("/v1/analytics/instagram/account-insights", params=params) - def get_instagram_demographics( - self, - account_id: str, - *, - metric: str | None = "follower_demographics", - breakdown: str | None = None, - timeframe: str | None = "this_month", - ) -> dict[str, Any]: + def get_instagram_demographics(self, account_id: str, *, metric: str | None = "follower_demographics", breakdown: str | None = None, timeframe: str | None = "this_month") -> dict[str, Any]: """Get Instagram audience demographics""" params = self._build_params( account_id=account_id, @@ -138,16 +95,7 @@ def get_instagram_demographics( ) return self._client._get("/v1/analytics/instagram/demographics", params=params) - def get_daily_metrics( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - account_id: str | None = None, - from_date: datetime | str | None = None, - to_date: datetime | str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + def get_daily_metrics(self, *, platform: str | None = None, profile_id: str | None = None, account_id: str | None = None, from_date: datetime | str | None = None, to_date: datetime | str | None = None, source: str | None = "all") -> dict[str, Any]: """Get daily aggregated metrics""" params = self._build_params( platform=platform, @@ -159,13 +107,7 @@ def get_daily_metrics( ) return self._client._get("/v1/analytics/daily-metrics", params=params) - def get_best_time_to_post( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + def get_best_time_to_post(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get best times to post""" params = self._build_params( platform=platform, @@ -174,13 +116,7 @@ def get_best_time_to_post( ) return self._client._get("/v1/analytics/best-time", params=params) - def get_content_decay( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + def get_content_decay(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get content performance decay""" params = self._build_params( platform=platform, @@ -189,13 +125,7 @@ def get_content_decay( ) return self._client._get("/v1/analytics/content-decay", params=params) - def get_posting_frequency( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + def get_posting_frequency(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get posting frequency vs engagement""" params = self._build_params( platform=platform, @@ -204,13 +134,7 @@ def get_posting_frequency( ) return self._client._get("/v1/analytics/posting-frequency", params=params) - def get_post_timeline( - self, - post_id: str, - *, - from_date: datetime | str | None = None, - to_date: datetime | str | None = None, - ) -> dict[str, Any]: + def get_post_timeline(self, post_id: str, *, from_date: datetime | str | None = None, to_date: datetime | str | None = None) -> dict[str, Any]: """Get post analytics timeline""" params = self._build_params( post_id=post_id, @@ -219,15 +143,7 @@ def get_post_timeline( ) return self._client._get("/v1/analytics/post-timeline", params=params) - def get_linked_in_aggregate_analytics( - self, - account_id: str, - *, - aggregation: str | None = "TOTAL", - start_date: str | None = None, - end_date: str | None = None, - metrics: str | None = None, - ) -> dict[str, Any]: + def get_linked_in_aggregate_analytics(self, account_id: str, *, aggregation: str | None = "TOTAL", start_date: str | None = None, end_date: str | None = None, metrics: str | None = None) -> dict[str, Any]: """Get LinkedIn aggregate stats""" params = self._build_params( aggregation=aggregation, @@ -235,52 +151,25 @@ def get_linked_in_aggregate_analytics( end_date=end_date, metrics=metrics, ) - return self._client._get( - f"/v1/accounts/{account_id}/linkedin-aggregate-analytics", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/linkedin-aggregate-analytics", params=params) def get_linked_in_post_analytics(self, account_id: str, urn: str) -> dict[str, Any]: """Get LinkedIn post stats""" params = self._build_params( urn=urn, ) - return self._client._get( - f"/v1/accounts/{account_id}/linkedin-post-analytics", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/linkedin-post-analytics", params=params) - def get_linked_in_post_reactions( - self, - account_id: str, - urn: str, - *, - limit: int | None = 25, - cursor: str | None = None, - ) -> dict[str, Any]: + def get_linked_in_post_reactions(self, account_id: str, urn: str, *, limit: int | None = 25, cursor: str | None = None) -> dict[str, Any]: """Get LinkedIn post reactions""" params = self._build_params( urn=urn, limit=limit, cursor=cursor, ) - return self._client._get( - f"/v1/accounts/{account_id}/linkedin-post-reactions", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/linkedin-post-reactions", params=params) - async def aget_analytics( - self, - *, - post_id: str | None = None, - platform: str | None = None, - profile_id: str | None = None, - account_id: str | None = None, - source: str | None = "all", - from_date: str | None = None, - to_date: str | None = None, - limit: int | None = 50, - page: int | None = 1, - sort_by: str | None = "date", - order: str | None = "desc", - ) -> dict[str, Any]: + async def aget_analytics(self, *, post_id: str | None = None, platform: str | None = None, profile_id: str | None = None, account_id: str | None = None, source: str | None = "all", from_date: str | None = None, to_date: str | None = None, limit: int | None = 50, page: int | None = 1, sort_by: str | None = "date", order: str | None = "desc") -> dict[str, Any]: """Get post analytics (async)""" params = self._build_params( post_id=post_id, @@ -297,14 +186,7 @@ async def aget_analytics( ) return await self._client._aget("/v1/analytics", params=params) - async def aget_you_tube_daily_views( - self, - video_id: str, - account_id: str, - *, - start_date: str | None = None, - end_date: str | None = None, - ) -> dict[str, Any]: + async def aget_you_tube_daily_views(self, video_id: str, account_id: str, *, start_date: str | None = None, end_date: str | None = None) -> dict[str, Any]: """Get YouTube daily views (async)""" params = self._build_params( video_id=video_id, @@ -312,20 +194,9 @@ async def aget_you_tube_daily_views( start_date=start_date, end_date=end_date, ) - return await self._client._aget( - "/v1/analytics/youtube/daily-views", params=params - ) + return await self._client._aget("/v1/analytics/youtube/daily-views", params=params) - async def aget_instagram_account_insights( - self, - account_id: str, - *, - metrics: str | None = None, - since: str | None = None, - until: str | None = None, - metric_type: str | None = "total_value", - breakdown: str | None = None, - ) -> dict[str, Any]: + async def aget_instagram_account_insights(self, account_id: str, *, metrics: str | None = None, since: str | None = None, until: str | None = None, metric_type: str | None = "total_value", breakdown: str | None = None) -> dict[str, Any]: """Get Instagram account-level insights (async)""" params = self._build_params( account_id=account_id, @@ -335,18 +206,9 @@ async def aget_instagram_account_insights( metric_type=metric_type, breakdown=breakdown, ) - return await self._client._aget( - "/v1/analytics/instagram/account-insights", params=params - ) + return await self._client._aget("/v1/analytics/instagram/account-insights", params=params) - async def aget_instagram_demographics( - self, - account_id: str, - *, - metric: str | None = "follower_demographics", - breakdown: str | None = None, - timeframe: str | None = "this_month", - ) -> dict[str, Any]: + async def aget_instagram_demographics(self, account_id: str, *, metric: str | None = "follower_demographics", breakdown: str | None = None, timeframe: str | None = "this_month") -> dict[str, Any]: """Get Instagram audience demographics (async)""" params = self._build_params( account_id=account_id, @@ -354,20 +216,9 @@ async def aget_instagram_demographics( breakdown=breakdown, timeframe=timeframe, ) - return await self._client._aget( - "/v1/analytics/instagram/demographics", params=params - ) + return await self._client._aget("/v1/analytics/instagram/demographics", params=params) - async def aget_daily_metrics( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - account_id: str | None = None, - from_date: datetime | str | None = None, - to_date: datetime | str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + async def aget_daily_metrics(self, *, platform: str | None = None, profile_id: str | None = None, account_id: str | None = None, from_date: datetime | str | None = None, to_date: datetime | str | None = None, source: str | None = "all") -> dict[str, Any]: """Get daily aggregated metrics (async)""" params = self._build_params( platform=platform, @@ -379,13 +230,7 @@ async def aget_daily_metrics( ) return await self._client._aget("/v1/analytics/daily-metrics", params=params) - async def aget_best_time_to_post( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + async def aget_best_time_to_post(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get best times to post (async)""" params = self._build_params( platform=platform, @@ -394,13 +239,7 @@ async def aget_best_time_to_post( ) return await self._client._aget("/v1/analytics/best-time", params=params) - async def aget_content_decay( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + async def aget_content_decay(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get content performance decay (async)""" params = self._build_params( platform=platform, @@ -409,30 +248,16 @@ async def aget_content_decay( ) return await self._client._aget("/v1/analytics/content-decay", params=params) - async def aget_posting_frequency( - self, - *, - platform: str | None = None, - profile_id: str | None = None, - source: str | None = "all", - ) -> dict[str, Any]: + async def aget_posting_frequency(self, *, platform: str | None = None, profile_id: str | None = None, source: str | None = "all") -> dict[str, Any]: """Get posting frequency vs engagement (async)""" params = self._build_params( platform=platform, profile_id=profile_id, source=source, ) - return await self._client._aget( - "/v1/analytics/posting-frequency", params=params - ) + return await self._client._aget("/v1/analytics/posting-frequency", params=params) - async def aget_post_timeline( - self, - post_id: str, - *, - from_date: datetime | str | None = None, - to_date: datetime | str | None = None, - ) -> dict[str, Any]: + async def aget_post_timeline(self, post_id: str, *, from_date: datetime | str | None = None, to_date: datetime | str | None = None) -> dict[str, Any]: """Get post analytics timeline (async)""" params = self._build_params( post_id=post_id, @@ -441,15 +266,7 @@ async def aget_post_timeline( ) return await self._client._aget("/v1/analytics/post-timeline", params=params) - async def aget_linked_in_aggregate_analytics( - self, - account_id: str, - *, - aggregation: str | None = "TOTAL", - start_date: str | None = None, - end_date: str | None = None, - metrics: str | None = None, - ) -> dict[str, Any]: + async def aget_linked_in_aggregate_analytics(self, account_id: str, *, aggregation: str | None = "TOTAL", start_date: str | None = None, end_date: str | None = None, metrics: str | None = None) -> dict[str, Any]: """Get LinkedIn aggregate stats (async)""" params = self._build_params( aggregation=aggregation, @@ -457,35 +274,20 @@ async def aget_linked_in_aggregate_analytics( end_date=end_date, metrics=metrics, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/linkedin-aggregate-analytics", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/linkedin-aggregate-analytics", params=params) - async def aget_linked_in_post_analytics( - self, account_id: str, urn: str - ) -> dict[str, Any]: + async def aget_linked_in_post_analytics(self, account_id: str, urn: str) -> dict[str, Any]: """Get LinkedIn post stats (async)""" params = self._build_params( urn=urn, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/linkedin-post-analytics", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/linkedin-post-analytics", params=params) - async def aget_linked_in_post_reactions( - self, - account_id: str, - urn: str, - *, - limit: int | None = 25, - cursor: str | None = None, - ) -> dict[str, Any]: + async def aget_linked_in_post_reactions(self, account_id: str, urn: str, *, limit: int | None = 25, cursor: str | None = None) -> dict[str, Any]: """Get LinkedIn post reactions (async)""" params = self._build_params( urn=urn, limit=limit, cursor=cursor, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/linkedin-post-reactions", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/linkedin-post-reactions", params=params) diff --git a/src/late/resources/_generated/api_keys.py b/src/late/resources/_generated/api_keys.py index 74edfe8..d8b91f3 100644 --- a/src/late/resources/_generated/api_keys.py +++ b/src/late/resources/_generated/api_keys.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -52,15 +48,7 @@ def list_api_keys(self) -> dict[str, Any]: """List keys""" return self._client._get("/v1/api-keys") - def create_api_key( - self, - name: str, - *, - expires_in: int | None = None, - scope: str | None = "full", - profile_ids: list[str] | None = None, - permission: str | None = "read-write", - ) -> dict[str, Any]: + def create_api_key(self, name: str, *, expires_in: int | None = None, scope: str | None = "full", profile_ids: list[str] | None = None, permission: str | None = "read-write") -> dict[str, Any]: """Create key""" payload = self._build_payload( name=name, @@ -79,15 +67,7 @@ async def alist_api_keys(self) -> dict[str, Any]: """List keys (async)""" return await self._client._aget("/v1/api-keys") - async def acreate_api_key( - self, - name: str, - *, - expires_in: int | None = None, - scope: str | None = "full", - profile_ids: list[str] | None = None, - permission: str | None = "read-write", - ) -> dict[str, Any]: + async def acreate_api_key(self, name: str, *, expires_in: int | None = None, scope: str | None = "full", profile_ids: list[str] | None = None, permission: str | None = "read-write") -> dict[str, Any]: """Create key (async)""" payload = self._build_payload( name=name, diff --git a/src/late/resources/_generated/broadcasts.py b/src/late/resources/_generated/broadcasts.py index f0999bd..7783808 100644 --- a/src/late/resources/_generated/broadcasts.py +++ b/src/late/resources/_generated/broadcasts.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,15 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_broadcasts( - self, - *, - profile_id: str | None = None, - status: str | None = None, - platform: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_broadcasts(self, *, profile_id: str | None = None, status: str | None = None, platform: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcasts""" params = self._build_params( profile_id=profile_id, @@ -69,18 +57,7 @@ def list_broadcasts( ) return self._client._get("/v1/broadcasts", params=params) - def create_broadcast( - self, - profile_id: str, - account_id: str, - platform: str, - name: str, - *, - description: str | None = None, - message: dict[str, Any] | None = None, - template: dict[str, Any] | None = None, - segment_filters: dict[str, Any] | None = None, - ) -> dict[str, Any]: + def create_broadcast(self, profile_id: str, account_id: str, platform: str, name: str, *, description: str | None = None, message: dict[str, Any] | None = None, template: dict[str, Any] | None = None, segment_filters: dict[str, Any] | None = None) -> dict[str, Any]: """Create a broadcast draft""" payload = self._build_payload( profile_id=profile_id, @@ -110,66 +87,36 @@ def send_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Trigger immediate send""" return self._client._post(f"/v1/broadcasts/{broadcast_id}/send") - def schedule_broadcast( - self, broadcast_id: str, scheduled_at: datetime | str - ) -> dict[str, Any]: + def schedule_broadcast(self, broadcast_id: str, scheduled_at: datetime | str) -> dict[str, Any]: """Schedule broadcast for later""" payload = self._build_payload( scheduled_at=scheduled_at, ) - return self._client._post( - f"/v1/broadcasts/{broadcast_id}/schedule", data=payload - ) + return self._client._post(f"/v1/broadcasts/{broadcast_id}/schedule", data=payload) def cancel_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Cancel a broadcast""" return self._client._post(f"/v1/broadcasts/{broadcast_id}/cancel") - def list_broadcast_recipients( - self, - broadcast_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_broadcast_recipients(self, broadcast_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcast recipients""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return self._client._get( - f"/v1/broadcasts/{broadcast_id}/recipients", params=params - ) + return self._client._get(f"/v1/broadcasts/{broadcast_id}/recipients", params=params) - def add_broadcast_recipients( - self, - broadcast_id: str, - *, - contact_ids: list[str] | None = None, - phones: list[str] | None = None, - use_segment: bool | None = None, - ) -> dict[str, Any]: + def add_broadcast_recipients(self, broadcast_id: str, *, contact_ids: list[str] | None = None, phones: list[str] | None = None, use_segment: bool | None = None) -> dict[str, Any]: """Add recipients to a broadcast""" payload = self._build_payload( contact_ids=contact_ids, phones=phones, use_segment=use_segment, ) - return self._client._post( - f"/v1/broadcasts/{broadcast_id}/recipients", data=payload - ) + return self._client._post(f"/v1/broadcasts/{broadcast_id}/recipients", data=payload) - async def alist_broadcasts( - self, - *, - profile_id: str | None = None, - status: str | None = None, - platform: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_broadcasts(self, *, profile_id: str | None = None, status: str | None = None, platform: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcasts (async)""" params = self._build_params( profile_id=profile_id, @@ -180,18 +127,7 @@ async def alist_broadcasts( ) return await self._client._aget("/v1/broadcasts", params=params) - async def acreate_broadcast( - self, - profile_id: str, - account_id: str, - platform: str, - name: str, - *, - description: str | None = None, - message: dict[str, Any] | None = None, - template: dict[str, Any] | None = None, - segment_filters: dict[str, Any] | None = None, - ) -> dict[str, Any]: + async def acreate_broadcast(self, profile_id: str, account_id: str, platform: str, name: str, *, description: str | None = None, message: dict[str, Any] | None = None, template: dict[str, Any] | None = None, segment_filters: dict[str, Any] | None = None) -> dict[str, Any]: """Create a broadcast draft (async)""" payload = self._build_payload( profile_id=profile_id, @@ -221,53 +157,31 @@ async def asend_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Trigger immediate send (async)""" return await self._client._apost(f"/v1/broadcasts/{broadcast_id}/send") - async def aschedule_broadcast( - self, broadcast_id: str, scheduled_at: datetime | str - ) -> dict[str, Any]: + async def aschedule_broadcast(self, broadcast_id: str, scheduled_at: datetime | str) -> dict[str, Any]: """Schedule broadcast for later (async)""" payload = self._build_payload( scheduled_at=scheduled_at, ) - return await self._client._apost( - f"/v1/broadcasts/{broadcast_id}/schedule", data=payload - ) + return await self._client._apost(f"/v1/broadcasts/{broadcast_id}/schedule", data=payload) async def acancel_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Cancel a broadcast (async)""" return await self._client._apost(f"/v1/broadcasts/{broadcast_id}/cancel") - async def alist_broadcast_recipients( - self, - broadcast_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_broadcast_recipients(self, broadcast_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcast recipients (async)""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return await self._client._aget( - f"/v1/broadcasts/{broadcast_id}/recipients", params=params - ) + return await self._client._aget(f"/v1/broadcasts/{broadcast_id}/recipients", params=params) - async def aadd_broadcast_recipients( - self, - broadcast_id: str, - *, - contact_ids: list[str] | None = None, - phones: list[str] | None = None, - use_segment: bool | None = None, - ) -> dict[str, Any]: + async def aadd_broadcast_recipients(self, broadcast_id: str, *, contact_ids: list[str] | None = None, phones: list[str] | None = None, use_segment: bool | None = None) -> dict[str, Any]: """Add recipients to a broadcast (async)""" payload = self._build_payload( contact_ids=contact_ids, phones=phones, use_segment=use_segment, ) - return await self._client._apost( - f"/v1/broadcasts/{broadcast_id}/recipients", data=payload - ) + return await self._client._apost(f"/v1/broadcasts/{broadcast_id}/recipients", data=payload) diff --git a/src/late/resources/_generated/comment_automations.py b/src/late/resources/_generated/comment_automations.py index 000e20a..49cc1f0 100644 --- a/src/late/resources/_generated/comment_automations.py +++ b/src/late/resources/_generated/comment_automations.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,29 +44,14 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_comment_automations( - self, *, profile_id: str | None = None - ) -> dict[str, Any]: + def list_comment_automations(self, *, profile_id: str | None = None) -> dict[str, Any]: """List comment-to-DM automations""" params = self._build_params( profile_id=profile_id, ) return self._client._get("/v1/comment-automations", params=params) - def create_comment_automation( - self, - profile_id: str, - account_id: str, - platform_post_id: str, - name: str, - dm_message: str, - *, - post_id: str | None = None, - post_title: str | None = None, - keywords: list[str] | None = None, - match_mode: str | None = "contains", - comment_reply: str | None = None, - ) -> dict[str, Any]: + def create_comment_automation(self, profile_id: str, account_id: str, platform_post_id: str, name: str, dm_message: str, *, post_id: str | None = None, post_title: str | None = None, keywords: list[str] | None = None, match_mode: str | None = "contains", comment_reply: str | None = None) -> dict[str, Any]: """Create a comment-to-DM automation""" payload = self._build_payload( profile_id=profile_id, @@ -90,17 +71,7 @@ def get_comment_automation(self, automation_id: str) -> dict[str, Any]: """Get automation details with recent logs""" return self._client._get(f"/v1/comment-automations/{automation_id}") - def update_comment_automation( - self, - automation_id: str, - *, - name: str | None = None, - keywords: list[str] | None = None, - match_mode: str | None = None, - dm_message: str | None = None, - comment_reply: str | None = None, - is_active: bool | None = None, - ) -> dict[str, Any]: + def update_comment_automation(self, automation_id: str, *, name: str | None = None, keywords: list[str] | None = None, match_mode: str | None = None, dm_message: str | None = None, comment_reply: str | None = None, is_active: bool | None = None) -> dict[str, Any]: """Update automation settings""" payload = self._build_payload( name=name, @@ -110,55 +81,29 @@ def update_comment_automation( comment_reply=comment_reply, is_active=is_active, ) - return self._client._patch( - f"/v1/comment-automations/{automation_id}", data=payload - ) + return self._client._patch(f"/v1/comment-automations/{automation_id}", data=payload) def delete_comment_automation(self, automation_id: str) -> dict[str, Any]: """Delete automation and all logs""" return self._client._delete(f"/v1/comment-automations/{automation_id}") - def list_comment_automation_logs( - self, - automation_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_comment_automation_logs(self, automation_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List trigger logs for an automation""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return self._client._get( - f"/v1/comment-automations/{automation_id}/logs", params=params - ) + return self._client._get(f"/v1/comment-automations/{automation_id}/logs", params=params) - async def alist_comment_automations( - self, *, profile_id: str | None = None - ) -> dict[str, Any]: + async def alist_comment_automations(self, *, profile_id: str | None = None) -> dict[str, Any]: """List comment-to-DM automations (async)""" params = self._build_params( profile_id=profile_id, ) return await self._client._aget("/v1/comment-automations", params=params) - async def acreate_comment_automation( - self, - profile_id: str, - account_id: str, - platform_post_id: str, - name: str, - dm_message: str, - *, - post_id: str | None = None, - post_title: str | None = None, - keywords: list[str] | None = None, - match_mode: str | None = "contains", - comment_reply: str | None = None, - ) -> dict[str, Any]: + async def acreate_comment_automation(self, profile_id: str, account_id: str, platform_post_id: str, name: str, dm_message: str, *, post_id: str | None = None, post_title: str | None = None, keywords: list[str] | None = None, match_mode: str | None = "contains", comment_reply: str | None = None) -> dict[str, Any]: """Create a comment-to-DM automation (async)""" payload = self._build_payload( profile_id=profile_id, @@ -178,17 +123,7 @@ async def aget_comment_automation(self, automation_id: str) -> dict[str, Any]: """Get automation details with recent logs (async)""" return await self._client._aget(f"/v1/comment-automations/{automation_id}") - async def aupdate_comment_automation( - self, - automation_id: str, - *, - name: str | None = None, - keywords: list[str] | None = None, - match_mode: str | None = None, - dm_message: str | None = None, - comment_reply: str | None = None, - is_active: bool | None = None, - ) -> dict[str, Any]: + async def aupdate_comment_automation(self, automation_id: str, *, name: str | None = None, keywords: list[str] | None = None, match_mode: str | None = None, dm_message: str | None = None, comment_reply: str | None = None, is_active: bool | None = None) -> dict[str, Any]: """Update automation settings (async)""" payload = self._build_payload( name=name, @@ -198,28 +133,17 @@ async def aupdate_comment_automation( comment_reply=comment_reply, is_active=is_active, ) - return await self._client._apatch( - f"/v1/comment-automations/{automation_id}", data=payload - ) + return await self._client._apatch(f"/v1/comment-automations/{automation_id}", data=payload) async def adelete_comment_automation(self, automation_id: str) -> dict[str, Any]: """Delete automation and all logs (async)""" return await self._client._adelete(f"/v1/comment-automations/{automation_id}") - async def alist_comment_automation_logs( - self, - automation_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_comment_automation_logs(self, automation_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List trigger logs for an automation (async)""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return await self._client._aget( - f"/v1/comment-automations/{automation_id}/logs", params=params - ) + return await self._client._aget(f"/v1/comment-automations/{automation_id}/logs", params=params) diff --git a/src/late/resources/_generated/comments.py b/src/late/resources/_generated/comments.py index 14a8cc3..b889e3d 100644 --- a/src/late/resources/_generated/comments.py +++ b/src/late/resources/_generated/comments.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,19 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_inbox_comments( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - min_comments: int | None = None, - since: datetime | str | None = None, - sort_by: str | None = "date", - sort_order: str | None = "desc", - limit: int | None = 50, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + def list_inbox_comments(self, *, profile_id: str | None = None, platform: str | None = None, min_comments: int | None = None, since: datetime | str | None = None, sort_by: str | None = "date", sort_order: str | None = "desc", limit: int | None = 50, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List commented posts""" params = self._build_params( profile_id=profile_id, @@ -77,16 +61,7 @@ def list_inbox_comments( ) return self._client._get("/v1/inbox/comments", params=params) - def get_inbox_post_comments( - self, - post_id: str, - account_id: str, - *, - subreddit: str | None = None, - limit: int | None = 25, - cursor: str | None = None, - comment_id: str | None = None, - ) -> dict[str, Any]: + def get_inbox_post_comments(self, post_id: str, account_id: str, *, subreddit: str | None = None, limit: int | None = 25, cursor: str | None = None, comment_id: str | None = None) -> dict[str, Any]: """Get post comments""" params = self._build_params( account_id=account_id, @@ -97,17 +72,7 @@ def get_inbox_post_comments( ) return self._client._get(f"/v1/inbox/comments/{post_id}", params=params) - def reply_to_inbox_post( - self, - post_id: str, - account_id: str, - message: str, - *, - comment_id: str | None = None, - parent_cid: str | None = None, - root_uri: str | None = None, - root_cid: str | None = None, - ) -> dict[str, Any]: + def reply_to_inbox_post(self, post_id: str, account_id: str, message: str, *, comment_id: str | None = None, parent_cid: str | None = None, root_uri: str | None = None, root_cid: str | None = None) -> dict[str, Any]: """Reply to comment""" payload = self._build_payload( account_id=account_id, @@ -119,9 +84,7 @@ def reply_to_inbox_post( ) return self._client._post(f"/v1/inbox/comments/{post_id}", data=payload) - def delete_inbox_comment( - self, post_id: str, account_id: str, comment_id: str - ) -> dict[str, Any]: + def delete_inbox_comment(self, post_id: str, account_id: str, comment_id: str) -> dict[str, Any]: """Delete comment""" params = self._build_params( account_id=account_id, @@ -129,82 +92,45 @@ def delete_inbox_comment( ) return self._client._delete(f"/v1/inbox/comments/{post_id}", params=params) - def hide_inbox_comment( - self, post_id: str, comment_id: str, account_id: str - ) -> dict[str, Any]: + def hide_inbox_comment(self, post_id: str, comment_id: str, account_id: str) -> dict[str, Any]: """Hide comment""" payload = self._build_payload( account_id=account_id, ) - return self._client._post( - f"/v1/inbox/comments/{post_id}/{comment_id}/hide", data=payload - ) + return self._client._post(f"/v1/inbox/comments/{post_id}/{comment_id}/hide", data=payload) - def unhide_inbox_comment( - self, post_id: str, comment_id: str, account_id: str - ) -> dict[str, Any]: + def unhide_inbox_comment(self, post_id: str, comment_id: str, account_id: str) -> dict[str, Any]: """Unhide comment""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/inbox/comments/{post_id}/{comment_id}/hide", params=params - ) + return self._client._delete(f"/v1/inbox/comments/{post_id}/{comment_id}/hide", params=params) - def like_inbox_comment( - self, post_id: str, comment_id: str, account_id: str, *, cid: str | None = None - ) -> dict[str, Any]: + def like_inbox_comment(self, post_id: str, comment_id: str, account_id: str, *, cid: str | None = None) -> dict[str, Any]: """Like comment""" payload = self._build_payload( account_id=account_id, cid=cid, ) - return self._client._post( - f"/v1/inbox/comments/{post_id}/{comment_id}/like", data=payload - ) + return self._client._post(f"/v1/inbox/comments/{post_id}/{comment_id}/like", data=payload) - def unlike_inbox_comment( - self, - post_id: str, - comment_id: str, - account_id: str, - *, - like_uri: str | None = None, - ) -> dict[str, Any]: + def unlike_inbox_comment(self, post_id: str, comment_id: str, account_id: str, *, like_uri: str | None = None) -> dict[str, Any]: """Unlike comment""" params = self._build_params( account_id=account_id, like_uri=like_uri, ) - return self._client._delete( - f"/v1/inbox/comments/{post_id}/{comment_id}/like", params=params - ) + return self._client._delete(f"/v1/inbox/comments/{post_id}/{comment_id}/like", params=params) - def send_private_reply_to_comment( - self, post_id: str, comment_id: str, account_id: str, message: str - ) -> dict[str, Any]: + def send_private_reply_to_comment(self, post_id: str, comment_id: str, account_id: str, message: str) -> dict[str, Any]: """Send private reply""" payload = self._build_payload( account_id=account_id, message=message, ) - return self._client._post( - f"/v1/inbox/comments/{post_id}/{comment_id}/private-reply", data=payload - ) + return self._client._post(f"/v1/inbox/comments/{post_id}/{comment_id}/private-reply", data=payload) - async def alist_inbox_comments( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - min_comments: int | None = None, - since: datetime | str | None = None, - sort_by: str | None = "date", - sort_order: str | None = "desc", - limit: int | None = 50, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + async def alist_inbox_comments(self, *, profile_id: str | None = None, platform: str | None = None, min_comments: int | None = None, since: datetime | str | None = None, sort_by: str | None = "date", sort_order: str | None = "desc", limit: int | None = 50, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List commented posts (async)""" params = self._build_params( profile_id=profile_id, @@ -219,16 +145,7 @@ async def alist_inbox_comments( ) return await self._client._aget("/v1/inbox/comments", params=params) - async def aget_inbox_post_comments( - self, - post_id: str, - account_id: str, - *, - subreddit: str | None = None, - limit: int | None = 25, - cursor: str | None = None, - comment_id: str | None = None, - ) -> dict[str, Any]: + async def aget_inbox_post_comments(self, post_id: str, account_id: str, *, subreddit: str | None = None, limit: int | None = 25, cursor: str | None = None, comment_id: str | None = None) -> dict[str, Any]: """Get post comments (async)""" params = self._build_params( account_id=account_id, @@ -239,17 +156,7 @@ async def aget_inbox_post_comments( ) return await self._client._aget(f"/v1/inbox/comments/{post_id}", params=params) - async def areply_to_inbox_post( - self, - post_id: str, - account_id: str, - message: str, - *, - comment_id: str | None = None, - parent_cid: str | None = None, - root_uri: str | None = None, - root_cid: str | None = None, - ) -> dict[str, Any]: + async def areply_to_inbox_post(self, post_id: str, account_id: str, message: str, *, comment_id: str | None = None, parent_cid: str | None = None, root_uri: str | None = None, root_cid: str | None = None) -> dict[str, Any]: """Reply to comment (async)""" payload = self._build_payload( account_id=account_id, @@ -261,77 +168,48 @@ async def areply_to_inbox_post( ) return await self._client._apost(f"/v1/inbox/comments/{post_id}", data=payload) - async def adelete_inbox_comment( - self, post_id: str, account_id: str, comment_id: str - ) -> dict[str, Any]: + async def adelete_inbox_comment(self, post_id: str, account_id: str, comment_id: str) -> dict[str, Any]: """Delete comment (async)""" params = self._build_params( account_id=account_id, comment_id=comment_id, ) - return await self._client._adelete( - f"/v1/inbox/comments/{post_id}", params=params - ) + return await self._client._adelete(f"/v1/inbox/comments/{post_id}", params=params) - async def ahide_inbox_comment( - self, post_id: str, comment_id: str, account_id: str - ) -> dict[str, Any]: + async def ahide_inbox_comment(self, post_id: str, comment_id: str, account_id: str) -> dict[str, Any]: """Hide comment (async)""" payload = self._build_payload( account_id=account_id, ) - return await self._client._apost( - f"/v1/inbox/comments/{post_id}/{comment_id}/hide", data=payload - ) + return await self._client._apost(f"/v1/inbox/comments/{post_id}/{comment_id}/hide", data=payload) - async def aunhide_inbox_comment( - self, post_id: str, comment_id: str, account_id: str - ) -> dict[str, Any]: + async def aunhide_inbox_comment(self, post_id: str, comment_id: str, account_id: str) -> dict[str, Any]: """Unhide comment (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/inbox/comments/{post_id}/{comment_id}/hide", params=params - ) + return await self._client._adelete(f"/v1/inbox/comments/{post_id}/{comment_id}/hide", params=params) - async def alike_inbox_comment( - self, post_id: str, comment_id: str, account_id: str, *, cid: str | None = None - ) -> dict[str, Any]: + async def alike_inbox_comment(self, post_id: str, comment_id: str, account_id: str, *, cid: str | None = None) -> dict[str, Any]: """Like comment (async)""" payload = self._build_payload( account_id=account_id, cid=cid, ) - return await self._client._apost( - f"/v1/inbox/comments/{post_id}/{comment_id}/like", data=payload - ) + return await self._client._apost(f"/v1/inbox/comments/{post_id}/{comment_id}/like", data=payload) - async def aunlike_inbox_comment( - self, - post_id: str, - comment_id: str, - account_id: str, - *, - like_uri: str | None = None, - ) -> dict[str, Any]: + async def aunlike_inbox_comment(self, post_id: str, comment_id: str, account_id: str, *, like_uri: str | None = None) -> dict[str, Any]: """Unlike comment (async)""" params = self._build_params( account_id=account_id, like_uri=like_uri, ) - return await self._client._adelete( - f"/v1/inbox/comments/{post_id}/{comment_id}/like", params=params - ) + return await self._client._adelete(f"/v1/inbox/comments/{post_id}/{comment_id}/like", params=params) - async def asend_private_reply_to_comment( - self, post_id: str, comment_id: str, account_id: str, message: str - ) -> dict[str, Any]: + async def asend_private_reply_to_comment(self, post_id: str, comment_id: str, account_id: str, message: str) -> dict[str, Any]: """Send private reply (async)""" payload = self._build_payload( account_id=account_id, message=message, ) - return await self._client._apost( - f"/v1/inbox/comments/{post_id}/{comment_id}/private-reply", data=payload - ) + return await self._client._apost(f"/v1/inbox/comments/{post_id}/{comment_id}/private-reply", data=payload) diff --git a/src/late/resources/_generated/connect.py b/src/late/resources/_generated/connect.py index 75c3407..5e0200e 100644 --- a/src/late/resources/_generated/connect.py +++ b/src/late/resources/_generated/connect.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,14 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def get_connect_url( - self, - platform: str, - profile_id: str, - *, - redirect_url: str | None = None, - headless: bool | None = False, - ) -> dict[str, Any]: + def get_connect_url(self, platform: str, profile_id: str, *, redirect_url: str | None = None, headless: bool | None = False) -> dict[str, Any]: """Get OAuth connect URL""" params = self._build_params( profile_id=profile_id, @@ -64,9 +53,7 @@ def get_connect_url( ) return self._client._get(f"/v1/connect/{platform}", params=params) - def handle_o_auth_callback( - self, platform: str, code: str, state: str, profile_id: str - ) -> dict[str, Any]: + def handle_o_auth_callback(self, platform: str, code: str, state: str, profile_id: str) -> dict[str, Any]: """Complete OAuth callback""" payload = self._build_payload( code=code, @@ -83,15 +70,7 @@ def list_facebook_pages(self, profile_id: str, temp_token: str) -> dict[str, Any ) return self._client._get("/v1/connect/facebook/select-page", params=params) - def select_facebook_page( - self, - profile_id: str, - page_id: str, - temp_token: str, - *, - user_profile: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + def select_facebook_page(self, profile_id: str, page_id: str, temp_token: str, *, user_profile: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Facebook page""" payload = self._build_payload( profile_id=profile_id, @@ -102,9 +81,7 @@ def select_facebook_page( ) return self._client._post("/v1/connect/facebook/select-page", data=payload) - def list_google_business_locations( - self, profile_id: str, temp_token: str - ) -> dict[str, Any]: + def list_google_business_locations(self, profile_id: str, temp_token: str) -> dict[str, Any]: """List GBP locations""" params = self._build_params( profile_id=profile_id, @@ -112,15 +89,7 @@ def list_google_business_locations( ) return self._client._get("/v1/connect/googlebusiness/locations", params=params) - def select_google_business_location( - self, - profile_id: str, - location_id: str, - temp_token: str, - *, - user_profile: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + def select_google_business_location(self, profile_id: str, location_id: str, temp_token: str, *, user_profile: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select GBP location""" payload = self._build_payload( profile_id=profile_id, @@ -129,9 +98,7 @@ def select_google_business_location( user_profile=user_profile, redirect_url=redirect_url, ) - return self._client._post( - "/v1/connect/googlebusiness/select-location", data=payload - ) + return self._client._post("/v1/connect/googlebusiness/select-location", data=payload) def get_pending_o_auth_data(self, token: str) -> dict[str, Any]: """Get pending OAuth data""" @@ -140,9 +107,7 @@ def get_pending_o_auth_data(self, token: str) -> dict[str, Any]: ) return self._client._get("/v1/connect/pending-data", params=params) - def list_linked_in_organizations( - self, temp_token: str, org_ids: str - ) -> dict[str, Any]: + def list_linked_in_organizations(self, temp_token: str, org_ids: str) -> dict[str, Any]: """List LinkedIn orgs""" params = self._build_params( temp_token=temp_token, @@ -150,16 +115,7 @@ def list_linked_in_organizations( ) return self._client._get("/v1/connect/linkedin/organizations", params=params) - def select_linked_in_organization( - self, - profile_id: str, - temp_token: str, - user_profile: dict[str, Any], - account_type: str, - *, - selected_organization: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + def select_linked_in_organization(self, profile_id: str, temp_token: str, user_profile: dict[str, Any], account_type: str, *, selected_organization: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select LinkedIn org""" payload = self._build_payload( profile_id=profile_id, @@ -169,13 +125,9 @@ def select_linked_in_organization( selected_organization=selected_organization, redirect_url=redirect_url, ) - return self._client._post( - "/v1/connect/linkedin/select-organization", data=payload - ) + return self._client._post("/v1/connect/linkedin/select-organization", data=payload) - def list_pinterest_boards_for_selection( - self, x_connect_token: str, profile_id: str, temp_token: str - ) -> dict[str, Any]: + def list_pinterest_boards_for_selection(self, x_connect_token: str, profile_id: str, temp_token: str) -> dict[str, Any]: """List Pinterest boards""" params = self._build_params( profile_id=profile_id, @@ -183,18 +135,7 @@ def list_pinterest_boards_for_selection( ) return self._client._get("/v1/connect/pinterest/select-board", params=params) - def select_pinterest_board( - self, - profile_id: str, - board_id: str, - temp_token: str, - *, - board_name: str | None = None, - user_profile: dict[str, Any] | None = None, - refresh_token: str | None = None, - expires_in: int | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + def select_pinterest_board(self, profile_id: str, board_id: str, temp_token: str, *, board_name: str | None = None, user_profile: dict[str, Any] | None = None, refresh_token: str | None = None, expires_in: int | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Pinterest board""" payload = self._build_payload( profile_id=profile_id, @@ -208,9 +149,7 @@ def select_pinterest_board( ) return self._client._post("/v1/connect/pinterest/select-board", data=payload) - def list_snapchat_profiles( - self, x_connect_token: str, profile_id: str, temp_token: str - ) -> dict[str, Any]: + def list_snapchat_profiles(self, x_connect_token: str, profile_id: str, temp_token: str) -> dict[str, Any]: """List Snapchat profiles""" params = self._build_params( profile_id=profile_id, @@ -218,18 +157,7 @@ def list_snapchat_profiles( ) return self._client._get("/v1/connect/snapchat/select-profile", params=params) - def select_snapchat_profile( - self, - profile_id: str, - selected_public_profile: dict[str, Any], - temp_token: str, - user_profile: dict[str, Any], - *, - x_connect_token: str | None = None, - refresh_token: str | None = None, - expires_in: int | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + def select_snapchat_profile(self, profile_id: str, selected_public_profile: dict[str, Any], temp_token: str, user_profile: dict[str, Any], *, x_connect_token: str | None = None, refresh_token: str | None = None, expires_in: int | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Snapchat profile""" payload = self._build_payload( profile_id=profile_id, @@ -242,14 +170,7 @@ def select_snapchat_profile( ) return self._client._post("/v1/connect/snapchat/select-profile", data=payload) - def connect_bluesky_credentials( - self, - identifier: str, - app_password: str, - state: str, - *, - redirect_uri: str | None = None, - ) -> dict[str, Any]: + def connect_bluesky_credentials(self, identifier: str, app_password: str, state: str, *, redirect_uri: str | None = None) -> dict[str, Any]: """Connect Bluesky account""" payload = self._build_payload( identifier=identifier, @@ -259,9 +180,7 @@ def connect_bluesky_credentials( ) return self._client._post("/v1/connect/bluesky/credentials", data=payload) - def connect_whats_app_credentials( - self, profile_id: str, access_token: str, waba_id: str, phone_number_id: str - ) -> dict[str, Any]: + def connect_whats_app_credentials(self, profile_id: str, access_token: str, waba_id: str, phone_number_id: str) -> dict[str, Any]: """Connect WhatsApp via credentials""" payload = self._build_payload( profile_id=profile_id, @@ -278,9 +197,7 @@ def get_telegram_connect_status(self, profile_id: str) -> dict[str, Any]: ) return self._client._get("/v1/connect/telegram", params=params) - def initiate_telegram_connect( - self, chat_id: str, profile_id: str - ) -> dict[str, Any]: + def initiate_telegram_connect(self, chat_id: str, profile_id: str) -> dict[str, Any]: """Connect Telegram directly""" payload = self._build_payload( chat_id=chat_id, @@ -299,124 +216,79 @@ def get_facebook_pages(self, account_id: str) -> dict[str, Any]: """List Facebook pages""" return self._client._get(f"/v1/accounts/{account_id}/facebook-page") - def update_facebook_page( - self, account_id: str, selected_page_id: str - ) -> dict[str, Any]: + def update_facebook_page(self, account_id: str, selected_page_id: str) -> dict[str, Any]: """Update Facebook page""" payload = self._build_payload( selected_page_id=selected_page_id, ) - return self._client._put( - f"/v1/accounts/{account_id}/facebook-page", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/facebook-page", data=payload) def get_linked_in_organizations(self, account_id: str) -> dict[str, Any]: """List LinkedIn orgs""" return self._client._get(f"/v1/accounts/{account_id}/linkedin-organizations") - def update_linked_in_organization( - self, - account_id: str, - account_type: str, - *, - selected_organization: dict[str, Any] | None = None, - ) -> dict[str, Any]: + def update_linked_in_organization(self, account_id: str, account_type: str, *, selected_organization: dict[str, Any] | None = None) -> dict[str, Any]: """Switch LinkedIn account type""" payload = self._build_payload( account_type=account_type, selected_organization=selected_organization, ) - return self._client._put( - f"/v1/accounts/{account_id}/linkedin-organization", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/linkedin-organization", data=payload) def get_pinterest_boards(self, account_id: str) -> dict[str, Any]: """List Pinterest boards""" return self._client._get(f"/v1/accounts/{account_id}/pinterest-boards") - def update_pinterest_boards( - self, - account_id: str, - default_board_id: str, - *, - default_board_name: str | None = None, - ) -> dict[str, Any]: + def update_pinterest_boards(self, account_id: str, default_board_id: str, *, default_board_name: str | None = None) -> dict[str, Any]: """Set default Pinterest board""" payload = self._build_payload( default_board_id=default_board_id, default_board_name=default_board_name, ) - return self._client._put( - f"/v1/accounts/{account_id}/pinterest-boards", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/pinterest-boards", data=payload) def get_youtube_playlists(self, account_id: str) -> dict[str, Any]: """List YouTube playlists""" return self._client._get(f"/v1/accounts/{account_id}/youtube-playlists") - def update_youtube_default_playlist( - self, - account_id: str, - default_playlist_id: str, - *, - default_playlist_name: str | None = None, - ) -> dict[str, Any]: + def update_youtube_default_playlist(self, account_id: str, default_playlist_id: str, *, default_playlist_name: str | None = None) -> dict[str, Any]: """Set default YouTube playlist""" payload = self._build_payload( default_playlist_id=default_playlist_id, default_playlist_name=default_playlist_name, ) - return self._client._put( - f"/v1/accounts/{account_id}/youtube-playlists", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/youtube-playlists", data=payload) def get_gmb_locations(self, account_id: str) -> dict[str, Any]: """List GBP locations""" return self._client._get(f"/v1/accounts/{account_id}/gmb-locations") - def update_gmb_location( - self, account_id: str, selected_location_id: str - ) -> dict[str, Any]: + def update_gmb_location(self, account_id: str, selected_location_id: str) -> dict[str, Any]: """Update GBP location""" payload = self._build_payload( selected_location_id=selected_location_id, ) - return self._client._put( - f"/v1/accounts/{account_id}/gmb-locations", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/gmb-locations", data=payload) def get_reddit_subreddits(self, account_id: str) -> dict[str, Any]: """List Reddit subreddits""" return self._client._get(f"/v1/accounts/{account_id}/reddit-subreddits") - def update_reddit_subreddits( - self, account_id: str, default_subreddit: str - ) -> dict[str, Any]: + def update_reddit_subreddits(self, account_id: str, default_subreddit: str) -> dict[str, Any]: """Set default subreddit""" payload = self._build_payload( default_subreddit=default_subreddit, ) - return self._client._put( - f"/v1/accounts/{account_id}/reddit-subreddits", data=payload - ) + return self._client._put(f"/v1/accounts/{account_id}/reddit-subreddits", data=payload) def get_reddit_flairs(self, account_id: str, subreddit: str) -> dict[str, Any]: """List subreddit flairs""" params = self._build_params( subreddit=subreddit, ) - return self._client._get( - f"/v1/accounts/{account_id}/reddit-flairs", params=params - ) + return self._client._get(f"/v1/accounts/{account_id}/reddit-flairs", params=params) - async def aget_connect_url( - self, - platform: str, - profile_id: str, - *, - redirect_url: str | None = None, - headless: bool | None = False, - ) -> dict[str, Any]: + async def aget_connect_url(self, platform: str, profile_id: str, *, redirect_url: str | None = None, headless: bool | None = False) -> dict[str, Any]: """Get OAuth connect URL (async)""" params = self._build_params( profile_id=profile_id, @@ -425,9 +297,7 @@ async def aget_connect_url( ) return await self._client._aget(f"/v1/connect/{platform}", params=params) - async def ahandle_o_auth_callback( - self, platform: str, code: str, state: str, profile_id: str - ) -> dict[str, Any]: + async def ahandle_o_auth_callback(self, platform: str, code: str, state: str, profile_id: str) -> dict[str, Any]: """Complete OAuth callback (async)""" payload = self._build_payload( code=code, @@ -436,27 +306,15 @@ async def ahandle_o_auth_callback( ) return await self._client._apost(f"/v1/connect/{platform}", data=payload) - async def alist_facebook_pages( - self, profile_id: str, temp_token: str - ) -> dict[str, Any]: + async def alist_facebook_pages(self, profile_id: str, temp_token: str) -> dict[str, Any]: """List Facebook pages (async)""" params = self._build_params( profile_id=profile_id, temp_token=temp_token, ) - return await self._client._aget( - "/v1/connect/facebook/select-page", params=params - ) + return await self._client._aget("/v1/connect/facebook/select-page", params=params) - async def aselect_facebook_page( - self, - profile_id: str, - page_id: str, - temp_token: str, - *, - user_profile: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + async def aselect_facebook_page(self, profile_id: str, page_id: str, temp_token: str, *, user_profile: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Facebook page (async)""" payload = self._build_payload( profile_id=profile_id, @@ -465,31 +323,17 @@ async def aselect_facebook_page( user_profile=user_profile, redirect_url=redirect_url, ) - return await self._client._apost( - "/v1/connect/facebook/select-page", data=payload - ) + return await self._client._apost("/v1/connect/facebook/select-page", data=payload) - async def alist_google_business_locations( - self, profile_id: str, temp_token: str - ) -> dict[str, Any]: + async def alist_google_business_locations(self, profile_id: str, temp_token: str) -> dict[str, Any]: """List GBP locations (async)""" params = self._build_params( profile_id=profile_id, temp_token=temp_token, ) - return await self._client._aget( - "/v1/connect/googlebusiness/locations", params=params - ) + return await self._client._aget("/v1/connect/googlebusiness/locations", params=params) - async def aselect_google_business_location( - self, - profile_id: str, - location_id: str, - temp_token: str, - *, - user_profile: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + async def aselect_google_business_location(self, profile_id: str, location_id: str, temp_token: str, *, user_profile: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select GBP location (async)""" payload = self._build_payload( profile_id=profile_id, @@ -498,9 +342,7 @@ async def aselect_google_business_location( user_profile=user_profile, redirect_url=redirect_url, ) - return await self._client._apost( - "/v1/connect/googlebusiness/select-location", data=payload - ) + return await self._client._apost("/v1/connect/googlebusiness/select-location", data=payload) async def aget_pending_o_auth_data(self, token: str) -> dict[str, Any]: """Get pending OAuth data (async)""" @@ -509,28 +351,15 @@ async def aget_pending_o_auth_data(self, token: str) -> dict[str, Any]: ) return await self._client._aget("/v1/connect/pending-data", params=params) - async def alist_linked_in_organizations( - self, temp_token: str, org_ids: str - ) -> dict[str, Any]: + async def alist_linked_in_organizations(self, temp_token: str, org_ids: str) -> dict[str, Any]: """List LinkedIn orgs (async)""" params = self._build_params( temp_token=temp_token, org_ids=org_ids, ) - return await self._client._aget( - "/v1/connect/linkedin/organizations", params=params - ) + return await self._client._aget("/v1/connect/linkedin/organizations", params=params) - async def aselect_linked_in_organization( - self, - profile_id: str, - temp_token: str, - user_profile: dict[str, Any], - account_type: str, - *, - selected_organization: dict[str, Any] | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + async def aselect_linked_in_organization(self, profile_id: str, temp_token: str, user_profile: dict[str, Any], account_type: str, *, selected_organization: dict[str, Any] | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select LinkedIn org (async)""" payload = self._build_payload( profile_id=profile_id, @@ -540,34 +369,17 @@ async def aselect_linked_in_organization( selected_organization=selected_organization, redirect_url=redirect_url, ) - return await self._client._apost( - "/v1/connect/linkedin/select-organization", data=payload - ) + return await self._client._apost("/v1/connect/linkedin/select-organization", data=payload) - async def alist_pinterest_boards_for_selection( - self, x_connect_token: str, profile_id: str, temp_token: str - ) -> dict[str, Any]: + async def alist_pinterest_boards_for_selection(self, x_connect_token: str, profile_id: str, temp_token: str) -> dict[str, Any]: """List Pinterest boards (async)""" params = self._build_params( profile_id=profile_id, temp_token=temp_token, ) - return await self._client._aget( - "/v1/connect/pinterest/select-board", params=params - ) - - async def aselect_pinterest_board( - self, - profile_id: str, - board_id: str, - temp_token: str, - *, - board_name: str | None = None, - user_profile: dict[str, Any] | None = None, - refresh_token: str | None = None, - expires_in: int | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + return await self._client._aget("/v1/connect/pinterest/select-board", params=params) + + async def aselect_pinterest_board(self, profile_id: str, board_id: str, temp_token: str, *, board_name: str | None = None, user_profile: dict[str, Any] | None = None, refresh_token: str | None = None, expires_in: int | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Pinterest board (async)""" payload = self._build_payload( profile_id=profile_id, @@ -579,34 +391,17 @@ async def aselect_pinterest_board( expires_in=expires_in, redirect_url=redirect_url, ) - return await self._client._apost( - "/v1/connect/pinterest/select-board", data=payload - ) + return await self._client._apost("/v1/connect/pinterest/select-board", data=payload) - async def alist_snapchat_profiles( - self, x_connect_token: str, profile_id: str, temp_token: str - ) -> dict[str, Any]: + async def alist_snapchat_profiles(self, x_connect_token: str, profile_id: str, temp_token: str) -> dict[str, Any]: """List Snapchat profiles (async)""" params = self._build_params( profile_id=profile_id, temp_token=temp_token, ) - return await self._client._aget( - "/v1/connect/snapchat/select-profile", params=params - ) - - async def aselect_snapchat_profile( - self, - profile_id: str, - selected_public_profile: dict[str, Any], - temp_token: str, - user_profile: dict[str, Any], - *, - x_connect_token: str | None = None, - refresh_token: str | None = None, - expires_in: int | None = None, - redirect_url: str | None = None, - ) -> dict[str, Any]: + return await self._client._aget("/v1/connect/snapchat/select-profile", params=params) + + async def aselect_snapchat_profile(self, profile_id: str, selected_public_profile: dict[str, Any], temp_token: str, user_profile: dict[str, Any], *, x_connect_token: str | None = None, refresh_token: str | None = None, expires_in: int | None = None, redirect_url: str | None = None) -> dict[str, Any]: """Select Snapchat profile (async)""" payload = self._build_payload( profile_id=profile_id, @@ -617,18 +412,9 @@ async def aselect_snapchat_profile( expires_in=expires_in, redirect_url=redirect_url, ) - return await self._client._apost( - "/v1/connect/snapchat/select-profile", data=payload - ) + return await self._client._apost("/v1/connect/snapchat/select-profile", data=payload) - async def aconnect_bluesky_credentials( - self, - identifier: str, - app_password: str, - state: str, - *, - redirect_uri: str | None = None, - ) -> dict[str, Any]: + async def aconnect_bluesky_credentials(self, identifier: str, app_password: str, state: str, *, redirect_uri: str | None = None) -> dict[str, Any]: """Connect Bluesky account (async)""" payload = self._build_payload( identifier=identifier, @@ -636,13 +422,9 @@ async def aconnect_bluesky_credentials( state=state, redirect_uri=redirect_uri, ) - return await self._client._apost( - "/v1/connect/bluesky/credentials", data=payload - ) + return await self._client._apost("/v1/connect/bluesky/credentials", data=payload) - async def aconnect_whats_app_credentials( - self, profile_id: str, access_token: str, waba_id: str, phone_number_id: str - ) -> dict[str, Any]: + async def aconnect_whats_app_credentials(self, profile_id: str, access_token: str, waba_id: str, phone_number_id: str) -> dict[str, Any]: """Connect WhatsApp via credentials (async)""" payload = self._build_payload( profile_id=profile_id, @@ -650,9 +432,7 @@ async def aconnect_whats_app_credentials( waba_id=waba_id, phone_number_id=phone_number_id, ) - return await self._client._apost( - "/v1/connect/whatsapp/credentials", data=payload - ) + return await self._client._apost("/v1/connect/whatsapp/credentials", data=payload) async def aget_telegram_connect_status(self, profile_id: str) -> dict[str, Any]: """Generate Telegram code (async)""" @@ -661,9 +441,7 @@ async def aget_telegram_connect_status(self, profile_id: str) -> dict[str, Any]: ) return await self._client._aget("/v1/connect/telegram", params=params) - async def ainitiate_telegram_connect( - self, chat_id: str, profile_id: str - ) -> dict[str, Any]: + async def ainitiate_telegram_connect(self, chat_id: str, profile_id: str) -> dict[str, Any]: """Connect Telegram directly (async)""" payload = self._build_payload( chat_id=chat_id, @@ -682,116 +460,74 @@ async def aget_facebook_pages(self, account_id: str) -> dict[str, Any]: """List Facebook pages (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/facebook-page") - async def aupdate_facebook_page( - self, account_id: str, selected_page_id: str - ) -> dict[str, Any]: + async def aupdate_facebook_page(self, account_id: str, selected_page_id: str) -> dict[str, Any]: """Update Facebook page (async)""" payload = self._build_payload( selected_page_id=selected_page_id, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/facebook-page", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/facebook-page", data=payload) async def aget_linked_in_organizations(self, account_id: str) -> dict[str, Any]: """List LinkedIn orgs (async)""" - return await self._client._aget( - f"/v1/accounts/{account_id}/linkedin-organizations" - ) + return await self._client._aget(f"/v1/accounts/{account_id}/linkedin-organizations") - async def aupdate_linked_in_organization( - self, - account_id: str, - account_type: str, - *, - selected_organization: dict[str, Any] | None = None, - ) -> dict[str, Any]: + async def aupdate_linked_in_organization(self, account_id: str, account_type: str, *, selected_organization: dict[str, Any] | None = None) -> dict[str, Any]: """Switch LinkedIn account type (async)""" payload = self._build_payload( account_type=account_type, selected_organization=selected_organization, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/linkedin-organization", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/linkedin-organization", data=payload) async def aget_pinterest_boards(self, account_id: str) -> dict[str, Any]: """List Pinterest boards (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/pinterest-boards") - async def aupdate_pinterest_boards( - self, - account_id: str, - default_board_id: str, - *, - default_board_name: str | None = None, - ) -> dict[str, Any]: + async def aupdate_pinterest_boards(self, account_id: str, default_board_id: str, *, default_board_name: str | None = None) -> dict[str, Any]: """Set default Pinterest board (async)""" payload = self._build_payload( default_board_id=default_board_id, default_board_name=default_board_name, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/pinterest-boards", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/pinterest-boards", data=payload) async def aget_youtube_playlists(self, account_id: str) -> dict[str, Any]: """List YouTube playlists (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/youtube-playlists") - async def aupdate_youtube_default_playlist( - self, - account_id: str, - default_playlist_id: str, - *, - default_playlist_name: str | None = None, - ) -> dict[str, Any]: + async def aupdate_youtube_default_playlist(self, account_id: str, default_playlist_id: str, *, default_playlist_name: str | None = None) -> dict[str, Any]: """Set default YouTube playlist (async)""" payload = self._build_payload( default_playlist_id=default_playlist_id, default_playlist_name=default_playlist_name, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/youtube-playlists", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/youtube-playlists", data=payload) async def aget_gmb_locations(self, account_id: str) -> dict[str, Any]: """List GBP locations (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/gmb-locations") - async def aupdate_gmb_location( - self, account_id: str, selected_location_id: str - ) -> dict[str, Any]: + async def aupdate_gmb_location(self, account_id: str, selected_location_id: str) -> dict[str, Any]: """Update GBP location (async)""" payload = self._build_payload( selected_location_id=selected_location_id, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/gmb-locations", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/gmb-locations", data=payload) async def aget_reddit_subreddits(self, account_id: str) -> dict[str, Any]: """List Reddit subreddits (async)""" return await self._client._aget(f"/v1/accounts/{account_id}/reddit-subreddits") - async def aupdate_reddit_subreddits( - self, account_id: str, default_subreddit: str - ) -> dict[str, Any]: + async def aupdate_reddit_subreddits(self, account_id: str, default_subreddit: str) -> dict[str, Any]: """Set default subreddit (async)""" payload = self._build_payload( default_subreddit=default_subreddit, ) - return await self._client._aput( - f"/v1/accounts/{account_id}/reddit-subreddits", data=payload - ) + return await self._client._aput(f"/v1/accounts/{account_id}/reddit-subreddits", data=payload) - async def aget_reddit_flairs( - self, account_id: str, subreddit: str - ) -> dict[str, Any]: + async def aget_reddit_flairs(self, account_id: str, subreddit: str) -> dict[str, Any]: """List subreddit flairs (async)""" params = self._build_params( subreddit=subreddit, ) - return await self._client._aget( - f"/v1/accounts/{account_id}/reddit-flairs", params=params - ) + return await self._client._aget(f"/v1/accounts/{account_id}/reddit-flairs", params=params) diff --git a/src/late/resources/_generated/contacts.py b/src/late/resources/_generated/contacts.py index 3c8b5b7..ad72c89 100644 --- a/src/late/resources/_generated/contacts.py +++ b/src/late/resources/_generated/contacts.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,17 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_contacts( - self, - *, - profile_id: str | None = None, - search: str | None = None, - tag: str | None = None, - platform: str | None = None, - is_subscribed: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_contacts(self, *, profile_id: str | None = None, search: str | None = None, tag: str | None = None, platform: str | None = None, is_subscribed: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List contacts""" params = self._build_params( profile_id=profile_id, @@ -71,21 +57,7 @@ def list_contacts( ) return self._client._get("/v1/contacts", params=params) - def create_contact( - self, - profile_id: str, - name: str, - *, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - is_subscribed: bool | None = True, - notes: str | None = None, - account_id: str | None = None, - platform: str | None = None, - platform_identifier: str | None = None, - display_identifier: str | None = None, - ) -> dict[str, Any]: + def create_contact(self, profile_id: str, name: str, *, email: str | None = None, company: str | None = None, tags: list[str] | None = None, is_subscribed: bool | None = True, notes: str | None = None, account_id: str | None = None, platform: str | None = None, platform_identifier: str | None = None, display_identifier: str | None = None) -> dict[str, Any]: """Create a contact""" payload = self._build_payload( profile_id=profile_id, @@ -106,19 +78,7 @@ def get_contact(self, contact_id: str) -> dict[str, Any]: """Get contact with channels""" return self._client._get(f"/v1/contacts/{contact_id}") - def update_contact( - self, - contact_id: str, - *, - name: str | None = None, - email: str | None = None, - company: str | None = None, - avatar_url: str | None = None, - tags: list[str] | None = None, - is_subscribed: bool | None = None, - is_blocked: bool | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + def update_contact(self, contact_id: str, *, name: str | None = None, email: str | None = None, company: str | None = None, avatar_url: str | None = None, tags: list[str] | None = None, is_subscribed: bool | None = None, is_blocked: bool | None = None, notes: str | None = None) -> dict[str, Any]: """Update a contact""" payload = self._build_payload( name=name, @@ -140,13 +100,7 @@ def get_contact_channels(self, contact_id: str) -> dict[str, Any]: """List channels for a contact""" return self._client._get(f"/v1/contacts/{contact_id}/channels") - def bulk_create_contacts( - self, - profile_id: str, - account_id: str, - platform: str, - contacts: list[dict[str, Any]], - ) -> dict[str, Any]: + def bulk_create_contacts(self, profile_id: str, account_id: str, platform: str, contacts: list[dict[str, Any]]) -> dict[str, Any]: """Bulk create contacts""" payload = self._build_payload( profile_id=profile_id, @@ -156,17 +110,7 @@ def bulk_create_contacts( ) return self._client._post("/v1/contacts/bulk", data=payload) - async def alist_contacts( - self, - *, - profile_id: str | None = None, - search: str | None = None, - tag: str | None = None, - platform: str | None = None, - is_subscribed: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_contacts(self, *, profile_id: str | None = None, search: str | None = None, tag: str | None = None, platform: str | None = None, is_subscribed: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List contacts (async)""" params = self._build_params( profile_id=profile_id, @@ -179,21 +123,7 @@ async def alist_contacts( ) return await self._client._aget("/v1/contacts", params=params) - async def acreate_contact( - self, - profile_id: str, - name: str, - *, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - is_subscribed: bool | None = True, - notes: str | None = None, - account_id: str | None = None, - platform: str | None = None, - platform_identifier: str | None = None, - display_identifier: str | None = None, - ) -> dict[str, Any]: + async def acreate_contact(self, profile_id: str, name: str, *, email: str | None = None, company: str | None = None, tags: list[str] | None = None, is_subscribed: bool | None = True, notes: str | None = None, account_id: str | None = None, platform: str | None = None, platform_identifier: str | None = None, display_identifier: str | None = None) -> dict[str, Any]: """Create a contact (async)""" payload = self._build_payload( profile_id=profile_id, @@ -214,19 +144,7 @@ async def aget_contact(self, contact_id: str) -> dict[str, Any]: """Get contact with channels (async)""" return await self._client._aget(f"/v1/contacts/{contact_id}") - async def aupdate_contact( - self, - contact_id: str, - *, - name: str | None = None, - email: str | None = None, - company: str | None = None, - avatar_url: str | None = None, - tags: list[str] | None = None, - is_subscribed: bool | None = None, - is_blocked: bool | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + async def aupdate_contact(self, contact_id: str, *, name: str | None = None, email: str | None = None, company: str | None = None, avatar_url: str | None = None, tags: list[str] | None = None, is_subscribed: bool | None = None, is_blocked: bool | None = None, notes: str | None = None) -> dict[str, Any]: """Update a contact (async)""" payload = self._build_payload( name=name, @@ -248,13 +166,7 @@ async def aget_contact_channels(self, contact_id: str) -> dict[str, Any]: """List channels for a contact (async)""" return await self._client._aget(f"/v1/contacts/{contact_id}/channels") - async def abulk_create_contacts( - self, - profile_id: str, - account_id: str, - platform: str, - contacts: list[dict[str, Any]], - ) -> dict[str, Any]: + async def abulk_create_contacts(self, profile_id: str, account_id: str, platform: str, contacts: list[dict[str, Any]]) -> dict[str, Any]: """Bulk create contacts (async)""" payload = self._build_payload( profile_id=profile_id, diff --git a/src/late/resources/_generated/custom_fields.py b/src/late/resources/_generated/custom_fields.py index 02d1678..81b80d2 100644 --- a/src/late/resources/_generated/custom_fields.py +++ b/src/late/resources/_generated/custom_fields.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,16 +44,12 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def set_contact_field_value( - self, contact_id: str, slug: str, value: Any - ) -> dict[str, Any]: + def set_contact_field_value(self, contact_id: str, slug: str, value: Any) -> dict[str, Any]: """Set a custom field value""" payload = self._build_payload( value=value, ) - return self._client._put( - f"/v1/contacts/{contact_id}/fields/{slug}", data=payload - ) + return self._client._put(f"/v1/contacts/{contact_id}/fields/{slug}", data=payload) def clear_contact_field_value(self, contact_id: str, slug: str) -> dict[str, Any]: """Clear a custom field value""" @@ -70,15 +62,7 @@ def list_custom_fields(self, *, profile_id: str | None = None) -> dict[str, Any] ) return self._client._get("/v1/custom-fields", params=params) - def create_custom_field( - self, - profile_id: str, - name: str, - type: str, - *, - slug: str | None = None, - options: list[str] | None = None, - ) -> dict[str, Any]: + def create_custom_field(self, profile_id: str, name: str, type: str, *, slug: str | None = None, options: list[str] | None = None) -> dict[str, Any]: """Create a custom field definition""" payload = self._build_payload( profile_id=profile_id, @@ -89,13 +73,7 @@ def create_custom_field( ) return self._client._post("/v1/custom-fields", data=payload) - def update_custom_field( - self, - field_id: str, - *, - name: str | None = None, - options: list[str] | None = None, - ) -> dict[str, Any]: + def update_custom_field(self, field_id: str, *, name: str | None = None, options: list[str] | None = None) -> dict[str, Any]: """Update a custom field definition""" payload = self._build_payload( name=name, @@ -107,41 +85,25 @@ def delete_custom_field(self, field_id: str) -> dict[str, Any]: """Delete a custom field definition""" return self._client._delete(f"/v1/custom-fields/{field_id}") - async def aset_contact_field_value( - self, contact_id: str, slug: str, value: Any - ) -> dict[str, Any]: + async def aset_contact_field_value(self, contact_id: str, slug: str, value: Any) -> dict[str, Any]: """Set a custom field value (async)""" payload = self._build_payload( value=value, ) - return await self._client._aput( - f"/v1/contacts/{contact_id}/fields/{slug}", data=payload - ) + return await self._client._aput(f"/v1/contacts/{contact_id}/fields/{slug}", data=payload) - async def aclear_contact_field_value( - self, contact_id: str, slug: str - ) -> dict[str, Any]: + async def aclear_contact_field_value(self, contact_id: str, slug: str) -> dict[str, Any]: """Clear a custom field value (async)""" return await self._client._adelete(f"/v1/contacts/{contact_id}/fields/{slug}") - async def alist_custom_fields( - self, *, profile_id: str | None = None - ) -> dict[str, Any]: + async def alist_custom_fields(self, *, profile_id: str | None = None) -> dict[str, Any]: """List custom field definitions (async)""" params = self._build_params( profile_id=profile_id, ) return await self._client._aget("/v1/custom-fields", params=params) - async def acreate_custom_field( - self, - profile_id: str, - name: str, - type: str, - *, - slug: str | None = None, - options: list[str] | None = None, - ) -> dict[str, Any]: + async def acreate_custom_field(self, profile_id: str, name: str, type: str, *, slug: str | None = None, options: list[str] | None = None) -> dict[str, Any]: """Create a custom field definition (async)""" payload = self._build_payload( profile_id=profile_id, @@ -152,13 +114,7 @@ async def acreate_custom_field( ) return await self._client._apost("/v1/custom-fields", data=payload) - async def aupdate_custom_field( - self, - field_id: str, - *, - name: str | None = None, - options: list[str] | None = None, - ) -> dict[str, Any]: + async def aupdate_custom_field(self, field_id: str, *, name: str | None = None, options: list[str] | None = None) -> dict[str, Any]: """Update a custom field definition (async)""" payload = self._build_payload( name=name, diff --git a/src/late/resources/_generated/invites.py b/src/late/resources/_generated/invites.py index d29eeb8..837b31a 100644 --- a/src/late/resources/_generated/invites.py +++ b/src/late/resources/_generated/invites.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,9 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def create_invite_token( - self, scope: str, *, profile_ids: list[str] | None = None - ) -> dict[str, Any]: + def create_invite_token(self, scope: str, *, profile_ids: list[str] | None = None) -> dict[str, Any]: """Create invite token""" payload = self._build_payload( scope=scope, @@ -58,9 +52,7 @@ def create_invite_token( ) return self._client._post("/v1/invite/tokens", data=payload) - async def acreate_invite_token( - self, scope: str, *, profile_ids: list[str] | None = None - ) -> dict[str, Any]: + async def acreate_invite_token(self, scope: str, *, profile_ids: list[str] | None = None) -> dict[str, Any]: """Create invite token (async)""" payload = self._build_payload( scope=scope, diff --git a/src/late/resources/_generated/logs.py b/src/late/resources/_generated/logs.py index a7577db..489a3f9 100644 --- a/src/late/resources/_generated/logs.py +++ b/src/late/resources/_generated/logs.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,17 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_posts_logs( - self, - *, - status: str | None = None, - platform: str | None = None, - action: str | None = None, - days: int | None = 7, - limit: int | None = 50, - skip: int | None = 0, - search: str | None = None, - ) -> dict[str, Any]: + def list_posts_logs(self, *, status: str | None = None, platform: str | None = None, action: str | None = None, days: int | None = 7, limit: int | None = 50, skip: int | None = 0, search: str | None = None) -> dict[str, Any]: """List publishing logs""" params = self._build_params( status=status, @@ -71,16 +57,7 @@ def list_posts_logs( ) return self._client._get("/v1/posts/logs", params=params) - def list_connection_logs( - self, - *, - platform: str | None = None, - event_type: str | None = None, - status: str | None = None, - days: int | None = 7, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_connection_logs(self, *, platform: str | None = None, event_type: str | None = None, status: str | None = None, days: int | None = 7, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List connection logs""" params = self._build_params( platform=platform, @@ -99,17 +76,7 @@ def get_post_logs(self, post_id: str, *, limit: int | None = 50) -> dict[str, An ) return self._client._get(f"/v1/posts/{post_id}/logs", params=params) - async def alist_posts_logs( - self, - *, - status: str | None = None, - platform: str | None = None, - action: str | None = None, - days: int | None = 7, - limit: int | None = 50, - skip: int | None = 0, - search: str | None = None, - ) -> dict[str, Any]: + async def alist_posts_logs(self, *, status: str | None = None, platform: str | None = None, action: str | None = None, days: int | None = 7, limit: int | None = 50, skip: int | None = 0, search: str | None = None) -> dict[str, Any]: """List publishing logs (async)""" params = self._build_params( status=status, @@ -122,16 +89,7 @@ async def alist_posts_logs( ) return await self._client._aget("/v1/posts/logs", params=params) - async def alist_connection_logs( - self, - *, - platform: str | None = None, - event_type: str | None = None, - status: str | None = None, - days: int | None = 7, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_connection_logs(self, *, platform: str | None = None, event_type: str | None = None, status: str | None = None, days: int | None = 7, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List connection logs (async)""" params = self._build_params( platform=platform, @@ -143,9 +101,7 @@ async def alist_connection_logs( ) return await self._client._aget("/v1/connections/logs", params=params) - async def aget_post_logs( - self, post_id: str, *, limit: int | None = 50 - ) -> dict[str, Any]: + async def aget_post_logs(self, post_id: str, *, limit: int | None = 50) -> dict[str, Any]: """Get post logs (async)""" params = self._build_params( limit=limit, diff --git a/src/late/resources/_generated/media.py b/src/late/resources/_generated/media.py index 6f99ecf..baeff0e 100644 --- a/src/late/resources/_generated/media.py +++ b/src/late/resources/_generated/media.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,9 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def get_media_presigned_url( - self, filename: str, content_type: str, *, size: int | None = None - ) -> dict[str, Any]: + def get_media_presigned_url(self, filename: str, content_type: str, *, size: int | None = None) -> dict[str, Any]: """Get presigned upload URL""" payload = self._build_payload( filename=filename, @@ -59,9 +53,7 @@ def get_media_presigned_url( ) return self._client._post("/v1/media/presign", data=payload) - async def aget_media_presigned_url( - self, filename: str, content_type: str, *, size: int | None = None - ) -> dict[str, Any]: + async def aget_media_presigned_url(self, filename: str, content_type: str, *, size: int | None = None) -> dict[str, Any]: """Get presigned upload URL (async)""" payload = self._build_payload( filename=filename, diff --git a/src/late/resources/_generated/messages.py b/src/late/resources/_generated/messages.py index ee5667c..5cc457d 100644 --- a/src/late/resources/_generated/messages.py +++ b/src/late/resources/_generated/messages.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,17 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_inbox_conversations( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - status: str | None = None, - sort_order: str | None = "desc", - limit: int | None = 50, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + def list_inbox_conversations(self, *, profile_id: str | None = None, platform: str | None = None, status: str | None = None, sort_order: str | None = "desc", limit: int | None = 50, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List conversations""" params = self._build_params( profile_id=profile_id, @@ -71,56 +57,29 @@ def list_inbox_conversations( ) return self._client._get("/v1/inbox/conversations", params=params) - def get_inbox_conversation( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + def get_inbox_conversation(self, conversation_id: str, account_id: str) -> dict[str, Any]: """Get conversation""" params = self._build_params( account_id=account_id, ) - return self._client._get( - f"/v1/inbox/conversations/{conversation_id}", params=params - ) + return self._client._get(f"/v1/inbox/conversations/{conversation_id}", params=params) - def update_inbox_conversation( - self, conversation_id: str, account_id: str, status: str - ) -> dict[str, Any]: + def update_inbox_conversation(self, conversation_id: str, account_id: str, status: str) -> dict[str, Any]: """Update conversation status""" payload = self._build_payload( account_id=account_id, status=status, ) - return self._client._put( - f"/v1/inbox/conversations/{conversation_id}", data=payload - ) + return self._client._put(f"/v1/inbox/conversations/{conversation_id}", data=payload) - def get_inbox_conversation_messages( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + def get_inbox_conversation_messages(self, conversation_id: str, account_id: str) -> dict[str, Any]: """List messages""" params = self._build_params( account_id=account_id, ) - return self._client._get( - f"/v1/inbox/conversations/{conversation_id}/messages", params=params - ) + return self._client._get(f"/v1/inbox/conversations/{conversation_id}/messages", params=params) - def send_inbox_message( - self, - conversation_id: str, - account_id: str, - *, - message: str | None = None, - attachment_url: str | None = None, - attachment_type: str | None = None, - quick_replies: list[dict[str, Any]] | None = None, - buttons: list[dict[str, Any]] | None = None, - template: dict[str, Any] | None = None, - reply_markup: dict[str, Any] | None = None, - messaging_type: str | None = None, - message_tag: str | None = None, - reply_to: str | None = None, - ) -> dict[str, Any]: + def send_inbox_message(self, conversation_id: str, account_id: str, *, message: str | None = None, attachment_url: str | None = None, attachment_type: str | None = None, quick_replies: list[dict[str, Any]] | None = None, buttons: list[dict[str, Any]] | None = None, template: dict[str, Any] | None = None, reply_markup: dict[str, Any] | None = None, messaging_type: str | None = None, message_tag: str | None = None, reply_to: str | None = None) -> dict[str, Any]: """Send message""" payload = self._build_payload( account_id=account_id, @@ -135,93 +94,51 @@ def send_inbox_message( message_tag=message_tag, reply_to=reply_to, ) - return self._client._post( - f"/v1/inbox/conversations/{conversation_id}/messages", data=payload - ) + return self._client._post(f"/v1/inbox/conversations/{conversation_id}/messages", data=payload) - def edit_inbox_message( - self, - conversation_id: str, - message_id: str, - account_id: str, - *, - text: str | None = None, - reply_markup: dict[str, Any] | None = None, - ) -> dict[str, Any]: + def edit_inbox_message(self, conversation_id: str, message_id: str, account_id: str, *, text: str | None = None, reply_markup: dict[str, Any] | None = None) -> dict[str, Any]: """Edit message""" payload = self._build_payload( account_id=account_id, text=text, reply_markup=reply_markup, ) - return self._client._patch( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", - data=payload, - ) + return self._client._patch(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", data=payload) - def delete_inbox_message( - self, conversation_id: str, message_id: str, account_id: str - ) -> dict[str, Any]: + def delete_inbox_message(self, conversation_id: str, message_id: str, account_id: str) -> dict[str, Any]: """Delete message""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", - params=params, - ) + return self._client._delete(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", params=params) - def send_typing_indicator( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + def send_typing_indicator(self, conversation_id: str, account_id: str) -> dict[str, Any]: """Send typing indicator""" payload = self._build_payload( account_id=account_id, ) - return self._client._post( - f"/v1/inbox/conversations/{conversation_id}/typing", data=payload - ) + return self._client._post(f"/v1/inbox/conversations/{conversation_id}/typing", data=payload) - def add_message_reaction( - self, conversation_id: str, message_id: str, account_id: str, emoji: str - ) -> dict[str, Any]: + def add_message_reaction(self, conversation_id: str, message_id: str, account_id: str, emoji: str) -> dict[str, Any]: """Add reaction""" payload = self._build_payload( account_id=account_id, emoji=emoji, ) - return self._client._post( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", - data=payload, - ) + return self._client._post(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", data=payload) - def remove_message_reaction( - self, conversation_id: str, message_id: str, account_id: str - ) -> dict[str, Any]: + def remove_message_reaction(self, conversation_id: str, message_id: str, account_id: str) -> dict[str, Any]: """Remove reaction""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", - params=params, - ) + return self._client._delete(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", params=params) def upload_media_direct(self) -> dict[str, Any]: """Upload media file""" return self._client._post("/v1/media/upload-direct") - async def alist_inbox_conversations( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - status: str | None = None, - sort_order: str | None = "desc", - limit: int | None = 50, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + async def alist_inbox_conversations(self, *, profile_id: str | None = None, platform: str | None = None, status: str | None = None, sort_order: str | None = "desc", limit: int | None = 50, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List conversations (async)""" params = self._build_params( profile_id=profile_id, @@ -234,56 +151,29 @@ async def alist_inbox_conversations( ) return await self._client._aget("/v1/inbox/conversations", params=params) - async def aget_inbox_conversation( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + async def aget_inbox_conversation(self, conversation_id: str, account_id: str) -> dict[str, Any]: """Get conversation (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._aget( - f"/v1/inbox/conversations/{conversation_id}", params=params - ) + return await self._client._aget(f"/v1/inbox/conversations/{conversation_id}", params=params) - async def aupdate_inbox_conversation( - self, conversation_id: str, account_id: str, status: str - ) -> dict[str, Any]: + async def aupdate_inbox_conversation(self, conversation_id: str, account_id: str, status: str) -> dict[str, Any]: """Update conversation status (async)""" payload = self._build_payload( account_id=account_id, status=status, ) - return await self._client._aput( - f"/v1/inbox/conversations/{conversation_id}", data=payload - ) + return await self._client._aput(f"/v1/inbox/conversations/{conversation_id}", data=payload) - async def aget_inbox_conversation_messages( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + async def aget_inbox_conversation_messages(self, conversation_id: str, account_id: str) -> dict[str, Any]: """List messages (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._aget( - f"/v1/inbox/conversations/{conversation_id}/messages", params=params - ) + return await self._client._aget(f"/v1/inbox/conversations/{conversation_id}/messages", params=params) - async def asend_inbox_message( - self, - conversation_id: str, - account_id: str, - *, - message: str | None = None, - attachment_url: str | None = None, - attachment_type: str | None = None, - quick_replies: list[dict[str, Any]] | None = None, - buttons: list[dict[str, Any]] | None = None, - template: dict[str, Any] | None = None, - reply_markup: dict[str, Any] | None = None, - messaging_type: str | None = None, - message_tag: str | None = None, - reply_to: str | None = None, - ) -> dict[str, Any]: + async def asend_inbox_message(self, conversation_id: str, account_id: str, *, message: str | None = None, attachment_url: str | None = None, attachment_type: str | None = None, quick_replies: list[dict[str, Any]] | None = None, buttons: list[dict[str, Any]] | None = None, template: dict[str, Any] | None = None, reply_markup: dict[str, Any] | None = None, messaging_type: str | None = None, message_tag: str | None = None, reply_to: str | None = None) -> dict[str, Any]: """Send message (async)""" payload = self._build_payload( account_id=account_id, @@ -298,77 +188,45 @@ async def asend_inbox_message( message_tag=message_tag, reply_to=reply_to, ) - return await self._client._apost( - f"/v1/inbox/conversations/{conversation_id}/messages", data=payload - ) + return await self._client._apost(f"/v1/inbox/conversations/{conversation_id}/messages", data=payload) - async def aedit_inbox_message( - self, - conversation_id: str, - message_id: str, - account_id: str, - *, - text: str | None = None, - reply_markup: dict[str, Any] | None = None, - ) -> dict[str, Any]: + async def aedit_inbox_message(self, conversation_id: str, message_id: str, account_id: str, *, text: str | None = None, reply_markup: dict[str, Any] | None = None) -> dict[str, Any]: """Edit message (async)""" payload = self._build_payload( account_id=account_id, text=text, reply_markup=reply_markup, ) - return await self._client._apatch( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", - data=payload, - ) + return await self._client._apatch(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", data=payload) - async def adelete_inbox_message( - self, conversation_id: str, message_id: str, account_id: str - ) -> dict[str, Any]: + async def adelete_inbox_message(self, conversation_id: str, message_id: str, account_id: str) -> dict[str, Any]: """Delete message (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", - params=params, - ) + return await self._client._adelete(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}", params=params) - async def asend_typing_indicator( - self, conversation_id: str, account_id: str - ) -> dict[str, Any]: + async def asend_typing_indicator(self, conversation_id: str, account_id: str) -> dict[str, Any]: """Send typing indicator (async)""" payload = self._build_payload( account_id=account_id, ) - return await self._client._apost( - f"/v1/inbox/conversations/{conversation_id}/typing", data=payload - ) + return await self._client._apost(f"/v1/inbox/conversations/{conversation_id}/typing", data=payload) - async def aadd_message_reaction( - self, conversation_id: str, message_id: str, account_id: str, emoji: str - ) -> dict[str, Any]: + async def aadd_message_reaction(self, conversation_id: str, message_id: str, account_id: str, emoji: str) -> dict[str, Any]: """Add reaction (async)""" payload = self._build_payload( account_id=account_id, emoji=emoji, ) - return await self._client._apost( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", - data=payload, - ) + return await self._client._apost(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", data=payload) - async def aremove_message_reaction( - self, conversation_id: str, message_id: str, account_id: str - ) -> dict[str, Any]: + async def aremove_message_reaction(self, conversation_id: str, message_id: str, account_id: str) -> dict[str, Any]: """Remove reaction (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", - params=params, - ) + return await self._client._adelete(f"/v1/inbox/conversations/{conversation_id}/messages/{message_id}/reactions", params=params) async def aupload_media_direct(self) -> dict[str, Any]: """Upload media file (async)""" diff --git a/src/late/resources/_generated/posts.py b/src/late/resources/_generated/posts.py index 638c8ab..62cb562 100644 --- a/src/late/resources/_generated/posts.py +++ b/src/late/resources/_generated/posts.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,21 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_posts( - self, - *, - page: int | None = 1, - limit: int | None = 10, - status: str | None = None, - platform: str | None = None, - profile_id: str | None = None, - created_by: str | None = None, - date_from: str | None = None, - date_to: str | None = None, - include_hidden: bool | None = False, - search: str | None = None, - sort_by: str | None = "scheduled-desc", - ) -> dict[str, Any]: + def list_posts(self, *, page: int | None = 1, limit: int | None = 10, status: str | None = None, platform: str | None = None, profile_id: str | None = None, created_by: str | None = None, date_from: str | None = None, date_to: str | None = None, include_hidden: bool | None = False, search: str | None = None, sort_by: str | None = "scheduled-desc") -> dict[str, Any]: """List posts""" params = self._build_params( page=page, @@ -81,27 +63,7 @@ def list_posts( ) return self._client._get("/v1/posts", params=params) - def create_post( - self, - *, - title: str | None = None, - content: str | None = None, - media_items: list[dict[str, Any]] | None = None, - platforms: list[dict[str, Any]] | None = None, - scheduled_for: datetime | str | None = None, - publish_now: bool | None = False, - is_draft: bool | None = False, - timezone: str | None = "UTC", - tags: list[str] | None = None, - hashtags: list[str] | None = None, - mentions: list[str] | None = None, - crossposting_enabled: bool | None = True, - metadata: dict[str, Any] | None = None, - tiktok_settings: Any | None = None, - recycling: Any | None = None, - queued_from_profile: str | None = None, - queue_id: str | None = None, - ) -> dict[str, Any]: + def create_post(self, *, title: str | None = None, content: str | None = None, media_items: list[dict[str, Any]] | None = None, platforms: list[dict[str, Any]] | None = None, scheduled_for: datetime | str | None = None, publish_now: bool | None = False, is_draft: bool | None = False, timezone: str | None = "UTC", tags: list[str] | None = None, hashtags: list[str] | None = None, mentions: list[str] | None = None, crossposting_enabled: bool | None = True, metadata: dict[str, Any] | None = None, tiktok_settings: Any | None = None, recycling: Any | None = None, queued_from_profile: str | None = None, queue_id: str | None = None) -> dict[str, Any]: """Create post""" payload = self._build_payload( title=title, @@ -128,15 +90,7 @@ def get_post(self, post_id: str) -> dict[str, Any]: """Get post""" return self._client._get(f"/v1/posts/{post_id}") - def update_post( - self, - post_id: str, - *, - content: str | None = None, - scheduled_for: datetime | str | None = None, - tiktok_settings: Any | None = None, - recycling: Any | None = None, - ) -> dict[str, Any]: + def update_post(self, post_id: str, *, content: str | None = None, scheduled_for: datetime | str | None = None, tiktok_settings: Any | None = None, recycling: Any | None = None) -> dict[str, Any]: """Update post""" payload = self._build_payload( content=content, @@ -168,23 +122,7 @@ def unpublish_post(self, post_id: str, platform: str) -> dict[str, Any]: ) return self._client._post(f"/v1/posts/{post_id}/unpublish", data=payload) - def update_post_metadata( - self, - post_id: str, - platform: str, - *, - video_id: str | None = None, - account_id: str | None = None, - title: str | None = None, - description: str | None = None, - tags: list[str] | None = None, - category_id: str | None = None, - privacy_status: str | None = None, - thumbnail_url: str | None = None, - made_for_kids: bool | None = None, - contains_synthetic_media: bool | None = None, - playlist_id: str | None = None, - ) -> dict[str, Any]: + def update_post_metadata(self, post_id: str, platform: str, *, video_id: str | None = None, account_id: str | None = None, title: str | None = None, description: str | None = None, tags: list[str] | None = None, category_id: str | None = None, privacy_status: str | None = None, thumbnail_url: str | None = None, made_for_kids: bool | None = None, contains_synthetic_media: bool | None = None, playlist_id: str | None = None) -> dict[str, Any]: """Update post metadata""" payload = self._build_payload( platform=platform, @@ -202,21 +140,7 @@ def update_post_metadata( ) return self._client._post(f"/v1/posts/{post_id}/update-metadata", data=payload) - async def alist_posts( - self, - *, - page: int | None = 1, - limit: int | None = 10, - status: str | None = None, - platform: str | None = None, - profile_id: str | None = None, - created_by: str | None = None, - date_from: str | None = None, - date_to: str | None = None, - include_hidden: bool | None = False, - search: str | None = None, - sort_by: str | None = "scheduled-desc", - ) -> dict[str, Any]: + async def alist_posts(self, *, page: int | None = 1, limit: int | None = 10, status: str | None = None, platform: str | None = None, profile_id: str | None = None, created_by: str | None = None, date_from: str | None = None, date_to: str | None = None, include_hidden: bool | None = False, search: str | None = None, sort_by: str | None = "scheduled-desc") -> dict[str, Any]: """List posts (async)""" params = self._build_params( page=page, @@ -233,27 +157,7 @@ async def alist_posts( ) return await self._client._aget("/v1/posts", params=params) - async def acreate_post( - self, - *, - title: str | None = None, - content: str | None = None, - media_items: list[dict[str, Any]] | None = None, - platforms: list[dict[str, Any]] | None = None, - scheduled_for: datetime | str | None = None, - publish_now: bool | None = False, - is_draft: bool | None = False, - timezone: str | None = "UTC", - tags: list[str] | None = None, - hashtags: list[str] | None = None, - mentions: list[str] | None = None, - crossposting_enabled: bool | None = True, - metadata: dict[str, Any] | None = None, - tiktok_settings: Any | None = None, - recycling: Any | None = None, - queued_from_profile: str | None = None, - queue_id: str | None = None, - ) -> dict[str, Any]: + async def acreate_post(self, *, title: str | None = None, content: str | None = None, media_items: list[dict[str, Any]] | None = None, platforms: list[dict[str, Any]] | None = None, scheduled_for: datetime | str | None = None, publish_now: bool | None = False, is_draft: bool | None = False, timezone: str | None = "UTC", tags: list[str] | None = None, hashtags: list[str] | None = None, mentions: list[str] | None = None, crossposting_enabled: bool | None = True, metadata: dict[str, Any] | None = None, tiktok_settings: Any | None = None, recycling: Any | None = None, queued_from_profile: str | None = None, queue_id: str | None = None) -> dict[str, Any]: """Create post (async)""" payload = self._build_payload( title=title, @@ -280,15 +184,7 @@ async def aget_post(self, post_id: str) -> dict[str, Any]: """Get post (async)""" return await self._client._aget(f"/v1/posts/{post_id}") - async def aupdate_post( - self, - post_id: str, - *, - content: str | None = None, - scheduled_for: datetime | str | None = None, - tiktok_settings: Any | None = None, - recycling: Any | None = None, - ) -> dict[str, Any]: + async def aupdate_post(self, post_id: str, *, content: str | None = None, scheduled_for: datetime | str | None = None, tiktok_settings: Any | None = None, recycling: Any | None = None) -> dict[str, Any]: """Update post (async)""" payload = self._build_payload( content=content, @@ -302,9 +198,7 @@ async def adelete_post(self, post_id: str) -> dict[str, Any]: """Delete post (async)""" return await self._client._adelete(f"/v1/posts/{post_id}") - async def abulk_upload_posts( - self, *, dry_run: bool | None = False - ) -> dict[str, Any]: + async def abulk_upload_posts(self, *, dry_run: bool | None = False) -> dict[str, Any]: """Bulk upload from CSV (async)""" params = self._build_params( dry_run=dry_run, @@ -322,23 +216,7 @@ async def aunpublish_post(self, post_id: str, platform: str) -> dict[str, Any]: ) return await self._client._apost(f"/v1/posts/{post_id}/unpublish", data=payload) - async def aupdate_post_metadata( - self, - post_id: str, - platform: str, - *, - video_id: str | None = None, - account_id: str | None = None, - title: str | None = None, - description: str | None = None, - tags: list[str] | None = None, - category_id: str | None = None, - privacy_status: str | None = None, - thumbnail_url: str | None = None, - made_for_kids: bool | None = None, - contains_synthetic_media: bool | None = None, - playlist_id: str | None = None, - ) -> dict[str, Any]: + async def aupdate_post_metadata(self, post_id: str, platform: str, *, video_id: str | None = None, account_id: str | None = None, title: str | None = None, description: str | None = None, tags: list[str] | None = None, category_id: str | None = None, privacy_status: str | None = None, thumbnail_url: str | None = None, made_for_kids: bool | None = None, contains_synthetic_media: bool | None = None, playlist_id: str | None = None) -> dict[str, Any]: """Update post metadata (async)""" payload = self._build_payload( platform=platform, @@ -354,6 +232,4 @@ async def aupdate_post_metadata( contains_synthetic_media=contains_synthetic_media, playlist_id=playlist_id, ) - return await self._client._apost( - f"/v1/posts/{post_id}/update-metadata", data=payload - ) + return await self._client._apost(f"/v1/posts/{post_id}/update-metadata", data=payload) diff --git a/src/late/resources/_generated/profiles.py b/src/late/resources/_generated/profiles.py index 5a34709..e62c300 100644 --- a/src/late/resources/_generated/profiles.py +++ b/src/late/resources/_generated/profiles.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,18 +44,14 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_profiles( - self, *, include_over_limit: bool | None = False - ) -> dict[str, Any]: + def list_profiles(self, *, include_over_limit: bool | None = False) -> dict[str, Any]: """List profiles""" params = self._build_params( include_over_limit=include_over_limit, ) return self._client._get("/v1/profiles", params=params) - def create_profile( - self, name: str, *, description: str | None = None, color: str | None = None - ) -> dict[str, Any]: + def create_profile(self, name: str, *, description: str | None = None, color: str | None = None) -> dict[str, Any]: """Create profile""" payload = self._build_payload( name=name, @@ -72,15 +64,7 @@ def get_profile(self, profile_id: str) -> dict[str, Any]: """Get profile""" return self._client._get(f"/v1/profiles/{profile_id}") - def update_profile( - self, - profile_id: str, - *, - name: str | None = None, - description: str | None = None, - color: str | None = None, - is_default: bool | None = None, - ) -> dict[str, Any]: + def update_profile(self, profile_id: str, *, name: str | None = None, description: str | None = None, color: str | None = None, is_default: bool | None = None) -> dict[str, Any]: """Update profile""" payload = self._build_payload( name=name, @@ -94,18 +78,14 @@ def delete_profile(self, profile_id: str) -> dict[str, Any]: """Delete profile""" return self._client._delete(f"/v1/profiles/{profile_id}") - async def alist_profiles( - self, *, include_over_limit: bool | None = False - ) -> dict[str, Any]: + async def alist_profiles(self, *, include_over_limit: bool | None = False) -> dict[str, Any]: """List profiles (async)""" params = self._build_params( include_over_limit=include_over_limit, ) return await self._client._aget("/v1/profiles", params=params) - async def acreate_profile( - self, name: str, *, description: str | None = None, color: str | None = None - ) -> dict[str, Any]: + async def acreate_profile(self, name: str, *, description: str | None = None, color: str | None = None) -> dict[str, Any]: """Create profile (async)""" payload = self._build_payload( name=name, @@ -118,15 +98,7 @@ async def aget_profile(self, profile_id: str) -> dict[str, Any]: """Get profile (async)""" return await self._client._aget(f"/v1/profiles/{profile_id}") - async def aupdate_profile( - self, - profile_id: str, - *, - name: str | None = None, - description: str | None = None, - color: str | None = None, - is_default: bool | None = None, - ) -> dict[str, Any]: + async def aupdate_profile(self, profile_id: str, *, name: str | None = None, description: str | None = None, color: str | None = None, is_default: bool | None = None) -> dict[str, Any]: """Update profile (async)""" payload = self._build_payload( name=name, diff --git a/src/late/resources/_generated/queue.py b/src/late/resources/_generated/queue.py index 158cb82..e81f86c 100644 --- a/src/late/resources/_generated/queue.py +++ b/src/late/resources/_generated/queue.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,9 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_queue_slots( - self, profile_id: str, *, queue_id: str | None = None, all: str | None = None - ) -> dict[str, Any]: + def list_queue_slots(self, profile_id: str, *, queue_id: str | None = None, all: str | None = None) -> dict[str, Any]: """List schedules""" params = self._build_params( profile_id=profile_id, @@ -59,15 +53,7 @@ def list_queue_slots( ) return self._client._get("/v1/queue/slots", params=params) - def create_queue_slot( - self, - profile_id: str, - name: str, - timezone: str, - slots: list[Any], - *, - active: bool | None = True, - ) -> dict[str, Any]: + def create_queue_slot(self, profile_id: str, name: str, timezone: str, slots: list[Any], *, active: bool | None = True) -> dict[str, Any]: """Create schedule""" payload = self._build_payload( profile_id=profile_id, @@ -78,18 +64,7 @@ def create_queue_slot( ) return self._client._post("/v1/queue/slots", data=payload) - def update_queue_slot( - self, - profile_id: str, - timezone: str, - slots: list[Any], - *, - queue_id: str | None = None, - name: str | None = None, - active: bool | None = True, - set_as_default: bool | None = None, - reshuffle_existing: bool | None = False, - ) -> dict[str, Any]: + def update_queue_slot(self, profile_id: str, timezone: str, slots: list[Any], *, queue_id: str | None = None, name: str | None = None, active: bool | None = True, set_as_default: bool | None = None, reshuffle_existing: bool | None = False) -> dict[str, Any]: """Update schedule""" payload = self._build_payload( profile_id=profile_id, @@ -111,9 +86,7 @@ def delete_queue_slot(self, profile_id: str, queue_id: str) -> dict[str, Any]: ) return self._client._delete("/v1/queue/slots", params=params) - def preview_queue( - self, profile_id: str, *, queue_id: str | None = None, count: int | None = 20 - ) -> dict[str, Any]: + def preview_queue(self, profile_id: str, *, queue_id: str | None = None, count: int | None = 20) -> dict[str, Any]: """Preview upcoming slots""" params = self._build_params( profile_id=profile_id, @@ -122,9 +95,7 @@ def preview_queue( ) return self._client._get("/v1/queue/preview", params=params) - def get_next_queue_slot( - self, profile_id: str, *, queue_id: str | None = None - ) -> dict[str, Any]: + def get_next_queue_slot(self, profile_id: str, *, queue_id: str | None = None) -> dict[str, Any]: """Get next available slot""" params = self._build_params( profile_id=profile_id, @@ -132,9 +103,7 @@ def get_next_queue_slot( ) return self._client._get("/v1/queue/next-slot", params=params) - async def alist_queue_slots( - self, profile_id: str, *, queue_id: str | None = None, all: str | None = None - ) -> dict[str, Any]: + async def alist_queue_slots(self, profile_id: str, *, queue_id: str | None = None, all: str | None = None) -> dict[str, Any]: """List schedules (async)""" params = self._build_params( profile_id=profile_id, @@ -143,15 +112,7 @@ async def alist_queue_slots( ) return await self._client._aget("/v1/queue/slots", params=params) - async def acreate_queue_slot( - self, - profile_id: str, - name: str, - timezone: str, - slots: list[Any], - *, - active: bool | None = True, - ) -> dict[str, Any]: + async def acreate_queue_slot(self, profile_id: str, name: str, timezone: str, slots: list[Any], *, active: bool | None = True) -> dict[str, Any]: """Create schedule (async)""" payload = self._build_payload( profile_id=profile_id, @@ -162,18 +123,7 @@ async def acreate_queue_slot( ) return await self._client._apost("/v1/queue/slots", data=payload) - async def aupdate_queue_slot( - self, - profile_id: str, - timezone: str, - slots: list[Any], - *, - queue_id: str | None = None, - name: str | None = None, - active: bool | None = True, - set_as_default: bool | None = None, - reshuffle_existing: bool | None = False, - ) -> dict[str, Any]: + async def aupdate_queue_slot(self, profile_id: str, timezone: str, slots: list[Any], *, queue_id: str | None = None, name: str | None = None, active: bool | None = True, set_as_default: bool | None = None, reshuffle_existing: bool | None = False) -> dict[str, Any]: """Update schedule (async)""" payload = self._build_payload( profile_id=profile_id, @@ -187,9 +137,7 @@ async def aupdate_queue_slot( ) return await self._client._aput("/v1/queue/slots", data=payload) - async def adelete_queue_slot( - self, profile_id: str, queue_id: str - ) -> dict[str, Any]: + async def adelete_queue_slot(self, profile_id: str, queue_id: str) -> dict[str, Any]: """Delete schedule (async)""" params = self._build_params( profile_id=profile_id, @@ -197,9 +145,7 @@ async def adelete_queue_slot( ) return await self._client._adelete("/v1/queue/slots", params=params) - async def apreview_queue( - self, profile_id: str, *, queue_id: str | None = None, count: int | None = 20 - ) -> dict[str, Any]: + async def apreview_queue(self, profile_id: str, *, queue_id: str | None = None, count: int | None = 20) -> dict[str, Any]: """Preview upcoming slots (async)""" params = self._build_params( profile_id=profile_id, @@ -208,9 +154,7 @@ async def apreview_queue( ) return await self._client._aget("/v1/queue/preview", params=params) - async def aget_next_queue_slot( - self, profile_id: str, *, queue_id: str | None = None - ) -> dict[str, Any]: + async def aget_next_queue_slot(self, profile_id: str, *, queue_id: str | None = None) -> dict[str, Any]: """Get next available slot (async)""" params = self._build_params( profile_id=profile_id, diff --git a/src/late/resources/_generated/reddit.py b/src/late/resources/_generated/reddit.py index 318c48c..3f28826 100644 --- a/src/late/resources/_generated/reddit.py +++ b/src/late/resources/_generated/reddit.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,17 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def search_reddit( - self, - account_id: str, - q: str, - *, - subreddit: str | None = None, - restrict_sr: str | None = None, - sort: str | None = "new", - limit: int | None = 25, - after: str | None = None, - ) -> dict[str, Any]: + def search_reddit(self, account_id: str, q: str, *, subreddit: str | None = None, restrict_sr: str | None = None, sort: str | None = "new", limit: int | None = 25, after: str | None = None) -> dict[str, Any]: """Search posts""" params = self._build_params( account_id=account_id, @@ -71,16 +57,7 @@ def search_reddit( ) return self._client._get("/v1/reddit/search", params=params) - def get_reddit_feed( - self, - account_id: str, - *, - subreddit: str | None = None, - sort: str | None = "hot", - limit: int | None = 25, - after: str | None = None, - t: str | None = None, - ) -> dict[str, Any]: + def get_reddit_feed(self, account_id: str, *, subreddit: str | None = None, sort: str | None = "hot", limit: int | None = 25, after: str | None = None, t: str | None = None) -> dict[str, Any]: """Get subreddit feed""" params = self._build_params( account_id=account_id, @@ -92,17 +69,7 @@ def get_reddit_feed( ) return self._client._get("/v1/reddit/feed", params=params) - async def asearch_reddit( - self, - account_id: str, - q: str, - *, - subreddit: str | None = None, - restrict_sr: str | None = None, - sort: str | None = "new", - limit: int | None = 25, - after: str | None = None, - ) -> dict[str, Any]: + async def asearch_reddit(self, account_id: str, q: str, *, subreddit: str | None = None, restrict_sr: str | None = None, sort: str | None = "new", limit: int | None = 25, after: str | None = None) -> dict[str, Any]: """Search posts (async)""" params = self._build_params( account_id=account_id, @@ -115,16 +82,7 @@ async def asearch_reddit( ) return await self._client._aget("/v1/reddit/search", params=params) - async def aget_reddit_feed( - self, - account_id: str, - *, - subreddit: str | None = None, - sort: str | None = "hot", - limit: int | None = 25, - after: str | None = None, - t: str | None = None, - ) -> dict[str, Any]: + async def aget_reddit_feed(self, account_id: str, *, subreddit: str | None = None, sort: str | None = "hot", limit: int | None = 25, after: str | None = None, t: str | None = None) -> dict[str, Any]: """Get subreddit feed (async)""" params = self._build_params( account_id=account_id, diff --git a/src/late/resources/_generated/reviews.py b/src/late/resources/_generated/reviews.py index 8d37ea2..058484a 100644 --- a/src/late/resources/_generated/reviews.py +++ b/src/late/resources/_generated/reviews.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,20 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_inbox_reviews( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - min_rating: int | None = None, - max_rating: int | None = None, - has_reply: bool | None = None, - sort_by: str | None = "date", - sort_order: str | None = "desc", - limit: int | None = 25, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + def list_inbox_reviews(self, *, profile_id: str | None = None, platform: str | None = None, min_rating: int | None = None, max_rating: int | None = None, has_reply: bool | None = None, sort_by: str | None = "date", sort_order: str | None = "desc", limit: int | None = 25, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List reviews""" params = self._build_params( profile_id=profile_id, @@ -77,9 +60,7 @@ def list_inbox_reviews( ) return self._client._get("/v1/inbox/reviews", params=params) - def reply_to_inbox_review( - self, review_id: str, account_id: str, message: str - ) -> dict[str, Any]: + def reply_to_inbox_review(self, review_id: str, account_id: str, message: str) -> dict[str, Any]: """Reply to review""" payload = self._build_payload( account_id=account_id, @@ -87,26 +68,11 @@ def reply_to_inbox_review( ) return self._client._post(f"/v1/inbox/reviews/{review_id}/reply", data=payload) - def delete_inbox_review_reply( - self, review_id: str, account_id: str - ) -> dict[str, Any]: + def delete_inbox_review_reply(self, review_id: str, account_id: str) -> dict[str, Any]: """Delete review reply""" return self._client._delete(f"/v1/inbox/reviews/{review_id}/reply") - async def alist_inbox_reviews( - self, - *, - profile_id: str | None = None, - platform: str | None = None, - min_rating: int | None = None, - max_rating: int | None = None, - has_reply: bool | None = None, - sort_by: str | None = "date", - sort_order: str | None = "desc", - limit: int | None = 25, - cursor: str | None = None, - account_id: str | None = None, - ) -> dict[str, Any]: + async def alist_inbox_reviews(self, *, profile_id: str | None = None, platform: str | None = None, min_rating: int | None = None, max_rating: int | None = None, has_reply: bool | None = None, sort_by: str | None = "date", sort_order: str | None = "desc", limit: int | None = 25, cursor: str | None = None, account_id: str | None = None) -> dict[str, Any]: """List reviews (async)""" params = self._build_params( profile_id=profile_id, @@ -122,20 +88,14 @@ async def alist_inbox_reviews( ) return await self._client._aget("/v1/inbox/reviews", params=params) - async def areply_to_inbox_review( - self, review_id: str, account_id: str, message: str - ) -> dict[str, Any]: + async def areply_to_inbox_review(self, review_id: str, account_id: str, message: str) -> dict[str, Any]: """Reply to review (async)""" payload = self._build_payload( account_id=account_id, message=message, ) - return await self._client._apost( - f"/v1/inbox/reviews/{review_id}/reply", data=payload - ) + return await self._client._apost(f"/v1/inbox/reviews/{review_id}/reply", data=payload) - async def adelete_inbox_review_reply( - self, review_id: str, account_id: str - ) -> dict[str, Any]: + async def adelete_inbox_review_reply(self, review_id: str, account_id: str) -> dict[str, Any]: """Delete review reply (async)""" return await self._client._adelete(f"/v1/inbox/reviews/{review_id}/reply") diff --git a/src/late/resources/_generated/sequences.py b/src/late/resources/_generated/sequences.py index 5cf74a4..4e443c4 100644 --- a/src/late/resources/_generated/sequences.py +++ b/src/late/resources/_generated/sequences.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,14 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def list_sequences( - self, - *, - profile_id: str | None = None, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_sequences(self, *, profile_id: str | None = None, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List sequences""" params = self._build_params( profile_id=profile_id, @@ -65,18 +54,7 @@ def list_sequences( ) return self._client._get("/v1/sequences", params=params) - def create_sequence( - self, - profile_id: str, - account_id: str, - platform: str, - name: str, - *, - description: str | None = None, - steps: list[dict[str, Any]] | None = None, - exit_on_reply: bool | None = True, - exit_on_unsubscribe: bool | None = True, - ) -> dict[str, Any]: + def create_sequence(self, profile_id: str, account_id: str, platform: str, name: str, *, description: str | None = None, steps: list[dict[str, Any]] | None = None, exit_on_reply: bool | None = True, exit_on_unsubscribe: bool | None = True) -> dict[str, Any]: """Create a sequence""" payload = self._build_payload( profile_id=profile_id, @@ -110,13 +88,7 @@ def pause_sequence(self, sequence_id: str) -> dict[str, Any]: """Pause a sequence""" return self._client._post(f"/v1/sequences/{sequence_id}/pause") - def enroll_contacts( - self, - sequence_id: str, - contact_ids: list[str], - *, - channel_ids: list[str] | None = None, - ) -> dict[str, Any]: + def enroll_contacts(self, sequence_id: str, contact_ids: list[str], *, channel_ids: list[str] | None = None) -> dict[str, Any]: """Enroll contacts in a sequence""" payload = self._build_payload( contact_ids=contact_ids, @@ -128,32 +100,16 @@ def unenroll_contact(self, sequence_id: str, contact_id: str) -> dict[str, Any]: """Unenroll a contact from a sequence""" return self._client._delete(f"/v1/sequences/{sequence_id}/enroll/{contact_id}") - def list_sequence_enrollments( - self, - sequence_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def list_sequence_enrollments(self, sequence_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List enrollments for a sequence""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return self._client._get( - f"/v1/sequences/{sequence_id}/enrollments", params=params - ) + return self._client._get(f"/v1/sequences/{sequence_id}/enrollments", params=params) - async def alist_sequences( - self, - *, - profile_id: str | None = None, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_sequences(self, *, profile_id: str | None = None, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List sequences (async)""" params = self._build_params( profile_id=profile_id, @@ -163,18 +119,7 @@ async def alist_sequences( ) return await self._client._aget("/v1/sequences", params=params) - async def acreate_sequence( - self, - profile_id: str, - account_id: str, - platform: str, - name: str, - *, - description: str | None = None, - steps: list[dict[str, Any]] | None = None, - exit_on_reply: bool | None = True, - exit_on_unsubscribe: bool | None = True, - ) -> dict[str, Any]: + async def acreate_sequence(self, profile_id: str, account_id: str, platform: str, name: str, *, description: str | None = None, steps: list[dict[str, Any]] | None = None, exit_on_reply: bool | None = True, exit_on_unsubscribe: bool | None = True) -> dict[str, Any]: """Create a sequence (async)""" payload = self._build_payload( profile_id=profile_id, @@ -208,44 +153,23 @@ async def apause_sequence(self, sequence_id: str) -> dict[str, Any]: """Pause a sequence (async)""" return await self._client._apost(f"/v1/sequences/{sequence_id}/pause") - async def aenroll_contacts( - self, - sequence_id: str, - contact_ids: list[str], - *, - channel_ids: list[str] | None = None, - ) -> dict[str, Any]: + async def aenroll_contacts(self, sequence_id: str, contact_ids: list[str], *, channel_ids: list[str] | None = None) -> dict[str, Any]: """Enroll contacts in a sequence (async)""" payload = self._build_payload( contact_ids=contact_ids, channel_ids=channel_ids, ) - return await self._client._apost( - f"/v1/sequences/{sequence_id}/enroll", data=payload - ) + return await self._client._apost(f"/v1/sequences/{sequence_id}/enroll", data=payload) - async def aunenroll_contact( - self, sequence_id: str, contact_id: str - ) -> dict[str, Any]: + async def aunenroll_contact(self, sequence_id: str, contact_id: str) -> dict[str, Any]: """Unenroll a contact from a sequence (async)""" - return await self._client._adelete( - f"/v1/sequences/{sequence_id}/enroll/{contact_id}" - ) + return await self._client._adelete(f"/v1/sequences/{sequence_id}/enroll/{contact_id}") - async def alist_sequence_enrollments( - self, - sequence_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def alist_sequence_enrollments(self, sequence_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List enrollments for a sequence (async)""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return await self._client._aget( - f"/v1/sequences/{sequence_id}/enrollments", params=params - ) + return await self._client._aget(f"/v1/sequences/{sequence_id}/enrollments", params=params) diff --git a/src/late/resources/_generated/twitter_engagement.py b/src/late/resources/_generated/twitter_engagement.py index d908215..08caf64 100644 --- a/src/late/resources/_generated/twitter_engagement.py +++ b/src/late/resources/_generated/twitter_engagement.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -128,9 +124,7 @@ async def aremove_bookmark(self, account_id: str, tweet_id: str) -> dict[str, An ) return await self._client._adelete("/v1/twitter/bookmark", params=params) - async def afollow_user( - self, account_id: str, target_user_id: str - ) -> dict[str, Any]: + async def afollow_user(self, account_id: str, target_user_id: str) -> dict[str, Any]: """Follow a user (async)""" payload = self._build_payload( account_id=account_id, @@ -138,9 +132,7 @@ async def afollow_user( ) return await self._client._apost("/v1/twitter/follow", data=payload) - async def aunfollow_user( - self, account_id: str, target_user_id: str - ) -> dict[str, Any]: + async def aunfollow_user(self, account_id: str, target_user_id: str) -> dict[str, Any]: """Unfollow a user (async)""" params = self._build_params( account_id=account_id, diff --git a/src/late/resources/_generated/usage.py b/src/late/resources/_generated/usage.py index d5aa576..469fb8c 100644 --- a/src/late/resources/_generated/usage.py +++ b/src/late/resources/_generated/usage.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: diff --git a/src/late/resources/_generated/users.py b/src/late/resources/_generated/users.py index 9c32545..139aa96 100644 --- a/src/late/resources/_generated/users.py +++ b/src/late/resources/_generated/users.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: diff --git a/src/late/resources/_generated/validate.py b/src/late/resources/_generated/validate.py index 988f4f4..741e6d2 100644 --- a/src/late/resources/_generated/validate.py +++ b/src/late/resources/_generated/validate.py @@ -15,7 +15,7 @@ class ValidateResource: """ - validate operations. + Content and subreddit validation tools. """ def __init__(self, client: BaseClient) -> None: @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -55,13 +51,7 @@ def validate_post_length(self, text: str) -> dict[str, Any]: ) return self._client._post("/v1/tools/validate/post-length", data=payload) - def validate_post( - self, - platforms: list[dict[str, Any]], - *, - content: str | None = None, - media_items: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + def validate_post(self, platforms: list[dict[str, Any]], *, content: str | None = None, media_items: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Validate post content""" payload = self._build_payload( content=content, @@ -91,13 +81,7 @@ async def avalidate_post_length(self, text: str) -> dict[str, Any]: ) return await self._client._apost("/v1/tools/validate/post-length", data=payload) - async def avalidate_post( - self, - platforms: list[dict[str, Any]], - *, - content: str | None = None, - media_items: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + async def avalidate_post(self, platforms: list[dict[str, Any]], *, content: str | None = None, media_items: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Validate post content (async)""" payload = self._build_payload( content=content, diff --git a/src/late/resources/_generated/webhooks.py b/src/late/resources/_generated/webhooks.py index 6994891..eb75d2f 100644 --- a/src/late/resources/_generated/webhooks.py +++ b/src/late/resources/_generated/webhooks.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -52,16 +48,7 @@ def get_webhook_settings(self) -> dict[str, Any]: """List webhooks""" return self._client._get("/v1/webhooks/settings") - def create_webhook_settings( - self, - *, - name: str | None = None, - url: str | None = None, - secret: str | None = None, - events: list[str] | None = None, - is_active: bool | None = None, - custom_headers: dict[str, Any] | None = None, - ) -> dict[str, Any]: + def create_webhook_settings(self, *, name: str | None = None, url: str | None = None, secret: str | None = None, events: list[str] | None = None, is_active: bool | None = None, custom_headers: dict[str, Any] | None = None) -> dict[str, Any]: """Create webhook""" payload = self._build_payload( name=name, @@ -73,17 +60,7 @@ def create_webhook_settings( ) return self._client._post("/v1/webhooks/settings", data=payload) - def update_webhook_settings( - self, - _id: str, - *, - name: str | None = None, - url: str | None = None, - secret: str | None = None, - events: list[str] | None = None, - is_active: bool | None = None, - custom_headers: dict[str, Any] | None = None, - ) -> dict[str, Any]: + def update_webhook_settings(self, _id: str, *, name: str | None = None, url: str | None = None, secret: str | None = None, events: list[str] | None = None, is_active: bool | None = None, custom_headers: dict[str, Any] | None = None) -> dict[str, Any]: """Update webhook""" payload = self._build_payload( _id=_id, @@ -110,14 +87,7 @@ def test_webhook(self, webhook_id: str) -> dict[str, Any]: ) return self._client._post("/v1/webhooks/test", data=payload) - def get_webhook_logs( - self, - *, - limit: int | None = 50, - status: str | None = None, - event: str | None = None, - webhook_id: str | None = None, - ) -> dict[str, Any]: + def get_webhook_logs(self, *, limit: int | None = 50, status: str | None = None, event: str | None = None, webhook_id: str | None = None) -> dict[str, Any]: """Get delivery logs""" params = self._build_params( limit=limit, @@ -131,16 +101,7 @@ async def aget_webhook_settings(self) -> dict[str, Any]: """List webhooks (async)""" return await self._client._aget("/v1/webhooks/settings") - async def acreate_webhook_settings( - self, - *, - name: str | None = None, - url: str | None = None, - secret: str | None = None, - events: list[str] | None = None, - is_active: bool | None = None, - custom_headers: dict[str, Any] | None = None, - ) -> dict[str, Any]: + async def acreate_webhook_settings(self, *, name: str | None = None, url: str | None = None, secret: str | None = None, events: list[str] | None = None, is_active: bool | None = None, custom_headers: dict[str, Any] | None = None) -> dict[str, Any]: """Create webhook (async)""" payload = self._build_payload( name=name, @@ -152,17 +113,7 @@ async def acreate_webhook_settings( ) return await self._client._apost("/v1/webhooks/settings", data=payload) - async def aupdate_webhook_settings( - self, - _id: str, - *, - name: str | None = None, - url: str | None = None, - secret: str | None = None, - events: list[str] | None = None, - is_active: bool | None = None, - custom_headers: dict[str, Any] | None = None, - ) -> dict[str, Any]: + async def aupdate_webhook_settings(self, _id: str, *, name: str | None = None, url: str | None = None, secret: str | None = None, events: list[str] | None = None, is_active: bool | None = None, custom_headers: dict[str, Any] | None = None) -> dict[str, Any]: """Update webhook (async)""" payload = self._build_payload( _id=_id, @@ -189,14 +140,7 @@ async def atest_webhook(self, webhook_id: str) -> dict[str, Any]: ) return await self._client._apost("/v1/webhooks/test", data=payload) - async def aget_webhook_logs( - self, - *, - limit: int | None = 50, - status: str | None = None, - event: str | None = None, - webhook_id: str | None = None, - ) -> dict[str, Any]: + async def aget_webhook_logs(self, *, limit: int | None = 50, status: str | None = None, event: str | None = None, webhook_id: str | None = None) -> dict[str, Any]: """Get delivery logs (async)""" params = self._build_params( limit=limit, diff --git a/src/late/resources/_generated/whatsapp.py b/src/late/resources/_generated/whatsapp.py index a962426..17405ee 100644 --- a/src/late/resources/_generated/whatsapp.py +++ b/src/late/resources/_generated/whatsapp.py @@ -25,21 +25,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -50,12 +46,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def send_whats_app_bulk( - self, - account_id: str, - recipients: list[dict[str, Any]], - template: dict[str, Any], - ) -> dict[str, Any]: + def send_whats_app_bulk(self, account_id: str, recipients: list[dict[str, Any]], template: dict[str, Any]) -> dict[str, Any]: """Bulk send template messages""" payload = self._build_payload( account_id=account_id, @@ -64,17 +55,7 @@ def send_whats_app_bulk( ) return self._client._post("/v1/whatsapp/bulk", data=payload) - def get_whats_app_contacts( - self, - account_id: str, - *, - search: str | None = None, - tag: str | None = None, - group: str | None = None, - opted_in: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def get_whats_app_contacts(self, account_id: str, *, search: str | None = None, tag: str | None = None, group: str | None = None, opted_in: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List contacts""" params = self._build_params( account_id=account_id, @@ -87,20 +68,7 @@ def get_whats_app_contacts( ) return self._client._get("/v1/whatsapp/contacts", params=params) - def create_whats_app_contact( - self, - account_id: str, - phone: str, - name: str, - *, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - groups: list[str] | None = None, - is_opted_in: bool | None = True, - custom_fields: dict[str, Any] | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + def create_whats_app_contact(self, account_id: str, phone: str, name: str, *, email: str | None = None, company: str | None = None, tags: list[str] | None = None, groups: list[str] | None = None, is_opted_in: bool | None = True, custom_fields: dict[str, Any] | None = None, notes: str | None = None) -> dict[str, Any]: """Create contact""" payload = self._build_payload( account_id=account_id, @@ -120,20 +88,7 @@ def get_whats_app_contact(self, contact_id: str) -> dict[str, Any]: """Get contact""" return self._client._get(f"/v1/whatsapp/contacts/{contact_id}") - def update_whats_app_contact( - self, - contact_id: str, - *, - name: str | None = None, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - groups: list[str] | None = None, - is_opted_in: bool | None = None, - is_blocked: bool | None = None, - custom_fields: dict[str, Any] | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + def update_whats_app_contact(self, contact_id: str, *, name: str | None = None, email: str | None = None, company: str | None = None, tags: list[str] | None = None, groups: list[str] | None = None, is_opted_in: bool | None = None, is_blocked: bool | None = None, custom_fields: dict[str, Any] | None = None, notes: str | None = None) -> dict[str, Any]: """Update contact""" payload = self._build_payload( name=name, @@ -152,15 +107,7 @@ def delete_whats_app_contact(self, contact_id: str) -> dict[str, Any]: """Delete contact""" return self._client._delete(f"/v1/whatsapp/contacts/{contact_id}") - def import_whats_app_contacts( - self, - account_id: str, - contacts: list[dict[str, Any]], - *, - default_tags: list[str] | None = None, - default_groups: list[str] | None = None, - skip_duplicates: bool | None = True, - ) -> dict[str, Any]: + def import_whats_app_contacts(self, account_id: str, contacts: list[dict[str, Any]], *, default_tags: list[str] | None = None, default_groups: list[str] | None = None, skip_duplicates: bool | None = True) -> dict[str, Any]: """Bulk import contacts""" payload = self._build_payload( account_id=account_id, @@ -171,14 +118,7 @@ def import_whats_app_contacts( ) return self._client._post("/v1/whatsapp/contacts/import", data=payload) - def bulk_update_whats_app_contacts( - self, - action: str, - contact_ids: list[str], - *, - tags: list[str] | None = None, - groups: list[str] | None = None, - ) -> dict[str, Any]: + def bulk_update_whats_app_contacts(self, action: str, contact_ids: list[str], *, tags: list[str] | None = None, groups: list[str] | None = None) -> dict[str, Any]: """Bulk update contacts""" payload = self._build_payload( action=action, @@ -199,9 +139,7 @@ def get_whats_app_groups(self, account_id: str) -> dict[str, Any]: ) return self._client._get("/v1/whatsapp/groups", params=params) - def rename_whats_app_group( - self, account_id: str, old_name: str, new_name: str - ) -> dict[str, Any]: + def rename_whats_app_group(self, account_id: str, old_name: str, new_name: str) -> dict[str, Any]: """Rename group""" payload = self._build_payload( account_id=account_id, @@ -210,9 +148,7 @@ def rename_whats_app_group( ) return self._client._post("/v1/whatsapp/groups", data=payload) - def delete_whats_app_group( - self, account_id: str, group_name: str - ) -> dict[str, Any]: + def delete_whats_app_group(self, account_id: str, group_name: str) -> dict[str, Any]: """Delete group""" return self._client._delete("/v1/whatsapp/groups") @@ -223,18 +159,7 @@ def get_whats_app_templates(self, account_id: str) -> dict[str, Any]: ) return self._client._get("/v1/whatsapp/templates", params=params) - def create_whats_app_template( - self, - account_id: str, - name: str, - category: str, - language: str, - *, - components: list[Any] | None = None, - library_template_name: str | None = None, - library_template_body_inputs: dict[str, Any] | None = None, - library_template_button_inputs: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + def create_whats_app_template(self, account_id: str, name: str, category: str, language: str, *, components: list[Any] | None = None, library_template_name: str | None = None, library_template_body_inputs: dict[str, Any] | None = None, library_template_button_inputs: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Create template""" payload = self._build_payload( account_id=account_id, @@ -248,48 +173,29 @@ def create_whats_app_template( ) return self._client._post("/v1/whatsapp/templates", data=payload) - def get_whats_app_template( - self, template_name: str, account_id: str - ) -> dict[str, Any]: + def get_whats_app_template(self, template_name: str, account_id: str) -> dict[str, Any]: """Get template""" params = self._build_params( account_id=account_id, ) - return self._client._get( - f"/v1/whatsapp/templates/{template_name}", params=params - ) + return self._client._get(f"/v1/whatsapp/templates/{template_name}", params=params) - def update_whats_app_template( - self, template_name: str, account_id: str, components: list[Any] - ) -> dict[str, Any]: + def update_whats_app_template(self, template_name: str, account_id: str, components: list[Any]) -> dict[str, Any]: """Update template""" payload = self._build_payload( account_id=account_id, components=components, ) - return self._client._patch( - f"/v1/whatsapp/templates/{template_name}", data=payload - ) + return self._client._patch(f"/v1/whatsapp/templates/{template_name}", data=payload) - def delete_whats_app_template( - self, template_name: str, account_id: str - ) -> dict[str, Any]: + def delete_whats_app_template(self, template_name: str, account_id: str) -> dict[str, Any]: """Delete template""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/whatsapp/templates/{template_name}", params=params - ) + return self._client._delete(f"/v1/whatsapp/templates/{template_name}", params=params) - def get_whats_app_broadcasts( - self, - account_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + def get_whats_app_broadcasts(self, account_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcasts""" params = self._build_params( account_id=account_id, @@ -299,15 +205,7 @@ def get_whats_app_broadcasts( ) return self._client._get("/v1/whatsapp/broadcasts", params=params) - def create_whats_app_broadcast( - self, - account_id: str, - name: str, - template: dict[str, Any], - *, - description: str | None = None, - recipients: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + def create_whats_app_broadcast(self, account_id: str, name: str, template: dict[str, Any], *, description: str | None = None, recipients: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Create broadcast""" payload = self._build_payload( account_id=account_id, @@ -330,57 +228,36 @@ def send_whats_app_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Send broadcast""" return self._client._post(f"/v1/whatsapp/broadcasts/{broadcast_id}/send") - def schedule_whats_app_broadcast( - self, broadcast_id: str, scheduled_at: datetime | str - ) -> dict[str, Any]: + def schedule_whats_app_broadcast(self, broadcast_id: str, scheduled_at: datetime | str) -> dict[str, Any]: """Schedule broadcast""" payload = self._build_payload( scheduled_at=scheduled_at, ) - return self._client._post( - f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule", data=payload - ) + return self._client._post(f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule", data=payload) def cancel_whats_app_broadcast_schedule(self, broadcast_id: str) -> dict[str, Any]: """Cancel scheduled broadcast""" return self._client._delete(f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule") - def get_whats_app_broadcast_recipients( - self, - broadcast_id: str, - *, - status: str | None = None, - limit: int | None = 100, - skip: int | None = 0, - ) -> dict[str, Any]: + def get_whats_app_broadcast_recipients(self, broadcast_id: str, *, status: str | None = None, limit: int | None = 100, skip: int | None = 0) -> dict[str, Any]: """List recipients""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return self._client._get( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", params=params - ) + return self._client._get(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", params=params) - def add_whats_app_broadcast_recipients( - self, broadcast_id: str, recipients: list[dict[str, Any]] - ) -> dict[str, Any]: + def add_whats_app_broadcast_recipients(self, broadcast_id: str, recipients: list[dict[str, Any]]) -> dict[str, Any]: """Add recipients""" payload = self._build_payload( recipients=recipients, ) - return self._client._patch( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", data=payload - ) + return self._client._patch(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", data=payload) - def remove_whats_app_broadcast_recipients( - self, broadcast_id: str, phones: list[str] - ) -> dict[str, Any]: + def remove_whats_app_broadcast_recipients(self, broadcast_id: str, phones: list[str]) -> dict[str, Any]: """Remove recipients""" - return self._client._delete( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients" - ) + return self._client._delete(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients") def get_whats_app_business_profile(self, account_id: str) -> dict[str, Any]: """Get business profile""" @@ -389,18 +266,7 @@ def get_whats_app_business_profile(self, account_id: str) -> dict[str, Any]: ) return self._client._get("/v1/whatsapp/business-profile", params=params) - def update_whats_app_business_profile( - self, - account_id: str, - *, - about: str | None = None, - address: str | None = None, - description: str | None = None, - email: str | None = None, - websites: list[str] | None = None, - vertical: str | None = None, - profile_picture_handle: str | None = None, - ) -> dict[str, Any]: + def update_whats_app_business_profile(self, account_id: str, *, about: str | None = None, address: str | None = None, description: str | None = None, email: str | None = None, websites: list[str] | None = None, vertical: str | None = None, profile_picture_handle: str | None = None) -> dict[str, Any]: """Update business profile""" payload = self._build_payload( account_id=account_id, @@ -423,25 +289,17 @@ def get_whats_app_display_name(self, account_id: str) -> dict[str, Any]: params = self._build_params( account_id=account_id, ) - return self._client._get( - "/v1/whatsapp/business-profile/display-name", params=params - ) + return self._client._get("/v1/whatsapp/business-profile/display-name", params=params) - def update_whats_app_display_name( - self, account_id: str, display_name: str - ) -> dict[str, Any]: + def update_whats_app_display_name(self, account_id: str, display_name: str) -> dict[str, Any]: """Request display name change""" payload = self._build_payload( account_id=account_id, display_name=display_name, ) - return self._client._post( - "/v1/whatsapp/business-profile/display-name", data=payload - ) + return self._client._post("/v1/whatsapp/business-profile/display-name", data=payload) - def list_whats_app_group_chats( - self, account_id: str, *, limit: int | None = 25, after: str | None = None - ) -> dict[str, Any]: + def list_whats_app_group_chats(self, account_id: str, *, limit: int | None = 25, after: str | None = None) -> dict[str, Any]: """List active groups""" params = self._build_params( account_id=account_id, @@ -450,14 +308,7 @@ def list_whats_app_group_chats( ) return self._client._get("/v1/whatsapp/wa-groups", params=params) - def create_whats_app_group_chat( - self, - account_id: str, - subject: str, - *, - description: str | None = None, - join_approval_mode: str | None = None, - ) -> dict[str, Any]: + def create_whats_app_group_chat(self, account_id: str, subject: str, *, description: str | None = None, join_approval_mode: str | None = None) -> dict[str, Any]: """Create group""" payload = self._build_payload( account_id=account_id, @@ -467,24 +318,14 @@ def create_whats_app_group_chat( ) return self._client._post("/v1/whatsapp/wa-groups", data=payload) - def get_whats_app_group_chat( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + def get_whats_app_group_chat(self, group_id: str, account_id: str) -> dict[str, Any]: """Get group info""" params = self._build_params( account_id=account_id, ) return self._client._get(f"/v1/whatsapp/wa-groups/{group_id}", params=params) - def update_whats_app_group_chat( - self, - group_id: str, - account_id: str, - *, - subject: str | None = None, - description: str | None = None, - join_approval_mode: str | None = None, - ) -> dict[str, Any]: + def update_whats_app_group_chat(self, group_id: str, account_id: str, *, subject: str | None = None, description: str | None = None, join_approval_mode: str | None = None) -> dict[str, Any]: """Update group settings""" payload = self._build_payload( subject=subject, @@ -493,87 +334,56 @@ def update_whats_app_group_chat( ) return self._client._post(f"/v1/whatsapp/wa-groups/{group_id}", data=payload) - def delete_whats_app_group_chat( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + def delete_whats_app_group_chat(self, group_id: str, account_id: str) -> dict[str, Any]: """Delete group""" params = self._build_params( account_id=account_id, ) return self._client._delete(f"/v1/whatsapp/wa-groups/{group_id}", params=params) - def add_whats_app_group_participants( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + def add_whats_app_group_participants(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Add participants""" payload = self._build_payload( phone_numbers=phone_numbers, ) - return self._client._post( - f"/v1/whatsapp/wa-groups/{group_id}/participants", data=payload - ) + return self._client._post(f"/v1/whatsapp/wa-groups/{group_id}/participants", data=payload) - def remove_whats_app_group_participants( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + def remove_whats_app_group_participants(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Remove participants""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/whatsapp/wa-groups/{group_id}/participants", params=params - ) + return self._client._delete(f"/v1/whatsapp/wa-groups/{group_id}/participants", params=params) - def create_whats_app_group_invite_link( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + def create_whats_app_group_invite_link(self, group_id: str, account_id: str) -> dict[str, Any]: """Create invite link""" params = self._build_params( account_id=account_id, ) - return self._client._post( - f"/v1/whatsapp/wa-groups/{group_id}/invite-link", params=params - ) + return self._client._post(f"/v1/whatsapp/wa-groups/{group_id}/invite-link", params=params) - def list_whats_app_group_join_requests( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + def list_whats_app_group_join_requests(self, group_id: str, account_id: str) -> dict[str, Any]: """List join requests""" params = self._build_params( account_id=account_id, ) - return self._client._get( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params - ) + return self._client._get(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params) - def approve_whats_app_group_join_requests( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + def approve_whats_app_group_join_requests(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Approve join requests""" payload = self._build_payload( phone_numbers=phone_numbers, ) - return self._client._post( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", data=payload - ) + return self._client._post(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", data=payload) - def reject_whats_app_group_join_requests( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + def reject_whats_app_group_join_requests(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Reject join requests""" params = self._build_params( account_id=account_id, ) - return self._client._delete( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params - ) + return self._client._delete(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params) - async def asend_whats_app_bulk( - self, - account_id: str, - recipients: list[dict[str, Any]], - template: dict[str, Any], - ) -> dict[str, Any]: + async def asend_whats_app_bulk(self, account_id: str, recipients: list[dict[str, Any]], template: dict[str, Any]) -> dict[str, Any]: """Bulk send template messages (async)""" payload = self._build_payload( account_id=account_id, @@ -582,17 +392,7 @@ async def asend_whats_app_bulk( ) return await self._client._apost("/v1/whatsapp/bulk", data=payload) - async def aget_whats_app_contacts( - self, - account_id: str, - *, - search: str | None = None, - tag: str | None = None, - group: str | None = None, - opted_in: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def aget_whats_app_contacts(self, account_id: str, *, search: str | None = None, tag: str | None = None, group: str | None = None, opted_in: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List contacts (async)""" params = self._build_params( account_id=account_id, @@ -605,20 +405,7 @@ async def aget_whats_app_contacts( ) return await self._client._aget("/v1/whatsapp/contacts", params=params) - async def acreate_whats_app_contact( - self, - account_id: str, - phone: str, - name: str, - *, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - groups: list[str] | None = None, - is_opted_in: bool | None = True, - custom_fields: dict[str, Any] | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + async def acreate_whats_app_contact(self, account_id: str, phone: str, name: str, *, email: str | None = None, company: str | None = None, tags: list[str] | None = None, groups: list[str] | None = None, is_opted_in: bool | None = True, custom_fields: dict[str, Any] | None = None, notes: str | None = None) -> dict[str, Any]: """Create contact (async)""" payload = self._build_payload( account_id=account_id, @@ -638,20 +425,7 @@ async def aget_whats_app_contact(self, contact_id: str) -> dict[str, Any]: """Get contact (async)""" return await self._client._aget(f"/v1/whatsapp/contacts/{contact_id}") - async def aupdate_whats_app_contact( - self, - contact_id: str, - *, - name: str | None = None, - email: str | None = None, - company: str | None = None, - tags: list[str] | None = None, - groups: list[str] | None = None, - is_opted_in: bool | None = None, - is_blocked: bool | None = None, - custom_fields: dict[str, Any] | None = None, - notes: str | None = None, - ) -> dict[str, Any]: + async def aupdate_whats_app_contact(self, contact_id: str, *, name: str | None = None, email: str | None = None, company: str | None = None, tags: list[str] | None = None, groups: list[str] | None = None, is_opted_in: bool | None = None, is_blocked: bool | None = None, custom_fields: dict[str, Any] | None = None, notes: str | None = None) -> dict[str, Any]: """Update contact (async)""" payload = self._build_payload( name=name, @@ -664,23 +438,13 @@ async def aupdate_whats_app_contact( custom_fields=custom_fields, notes=notes, ) - return await self._client._aput( - f"/v1/whatsapp/contacts/{contact_id}", data=payload - ) + return await self._client._aput(f"/v1/whatsapp/contacts/{contact_id}", data=payload) async def adelete_whats_app_contact(self, contact_id: str) -> dict[str, Any]: """Delete contact (async)""" return await self._client._adelete(f"/v1/whatsapp/contacts/{contact_id}") - async def aimport_whats_app_contacts( - self, - account_id: str, - contacts: list[dict[str, Any]], - *, - default_tags: list[str] | None = None, - default_groups: list[str] | None = None, - skip_duplicates: bool | None = True, - ) -> dict[str, Any]: + async def aimport_whats_app_contacts(self, account_id: str, contacts: list[dict[str, Any]], *, default_tags: list[str] | None = None, default_groups: list[str] | None = None, skip_duplicates: bool | None = True) -> dict[str, Any]: """Bulk import contacts (async)""" payload = self._build_payload( account_id=account_id, @@ -691,14 +455,7 @@ async def aimport_whats_app_contacts( ) return await self._client._apost("/v1/whatsapp/contacts/import", data=payload) - async def abulk_update_whats_app_contacts( - self, - action: str, - contact_ids: list[str], - *, - tags: list[str] | None = None, - groups: list[str] | None = None, - ) -> dict[str, Any]: + async def abulk_update_whats_app_contacts(self, action: str, contact_ids: list[str], *, tags: list[str] | None = None, groups: list[str] | None = None) -> dict[str, Any]: """Bulk update contacts (async)""" payload = self._build_payload( action=action, @@ -708,9 +465,7 @@ async def abulk_update_whats_app_contacts( ) return await self._client._apost("/v1/whatsapp/contacts/bulk", data=payload) - async def abulk_delete_whats_app_contacts( - self, contact_ids: list[str] - ) -> dict[str, Any]: + async def abulk_delete_whats_app_contacts(self, contact_ids: list[str]) -> dict[str, Any]: """Bulk delete contacts (async)""" return await self._client._adelete("/v1/whatsapp/contacts/bulk") @@ -721,9 +476,7 @@ async def aget_whats_app_groups(self, account_id: str) -> dict[str, Any]: ) return await self._client._aget("/v1/whatsapp/groups", params=params) - async def arename_whats_app_group( - self, account_id: str, old_name: str, new_name: str - ) -> dict[str, Any]: + async def arename_whats_app_group(self, account_id: str, old_name: str, new_name: str) -> dict[str, Any]: """Rename group (async)""" payload = self._build_payload( account_id=account_id, @@ -732,9 +485,7 @@ async def arename_whats_app_group( ) return await self._client._apost("/v1/whatsapp/groups", data=payload) - async def adelete_whats_app_group( - self, account_id: str, group_name: str - ) -> dict[str, Any]: + async def adelete_whats_app_group(self, account_id: str, group_name: str) -> dict[str, Any]: """Delete group (async)""" return await self._client._adelete("/v1/whatsapp/groups") @@ -745,18 +496,7 @@ async def aget_whats_app_templates(self, account_id: str) -> dict[str, Any]: ) return await self._client._aget("/v1/whatsapp/templates", params=params) - async def acreate_whats_app_template( - self, - account_id: str, - name: str, - category: str, - language: str, - *, - components: list[Any] | None = None, - library_template_name: str | None = None, - library_template_body_inputs: dict[str, Any] | None = None, - library_template_button_inputs: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + async def acreate_whats_app_template(self, account_id: str, name: str, category: str, language: str, *, components: list[Any] | None = None, library_template_name: str | None = None, library_template_body_inputs: dict[str, Any] | None = None, library_template_button_inputs: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Create template (async)""" payload = self._build_payload( account_id=account_id, @@ -770,48 +510,29 @@ async def acreate_whats_app_template( ) return await self._client._apost("/v1/whatsapp/templates", data=payload) - async def aget_whats_app_template( - self, template_name: str, account_id: str - ) -> dict[str, Any]: + async def aget_whats_app_template(self, template_name: str, account_id: str) -> dict[str, Any]: """Get template (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._aget( - f"/v1/whatsapp/templates/{template_name}", params=params - ) + return await self._client._aget(f"/v1/whatsapp/templates/{template_name}", params=params) - async def aupdate_whats_app_template( - self, template_name: str, account_id: str, components: list[Any] - ) -> dict[str, Any]: + async def aupdate_whats_app_template(self, template_name: str, account_id: str, components: list[Any]) -> dict[str, Any]: """Update template (async)""" payload = self._build_payload( account_id=account_id, components=components, ) - return await self._client._apatch( - f"/v1/whatsapp/templates/{template_name}", data=payload - ) + return await self._client._apatch(f"/v1/whatsapp/templates/{template_name}", data=payload) - async def adelete_whats_app_template( - self, template_name: str, account_id: str - ) -> dict[str, Any]: + async def adelete_whats_app_template(self, template_name: str, account_id: str) -> dict[str, Any]: """Delete template (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/whatsapp/templates/{template_name}", params=params - ) + return await self._client._adelete(f"/v1/whatsapp/templates/{template_name}", params=params) - async def aget_whats_app_broadcasts( - self, - account_id: str, - *, - status: str | None = None, - limit: int | None = 50, - skip: int | None = 0, - ) -> dict[str, Any]: + async def aget_whats_app_broadcasts(self, account_id: str, *, status: str | None = None, limit: int | None = 50, skip: int | None = 0) -> dict[str, Any]: """List broadcasts (async)""" params = self._build_params( account_id=account_id, @@ -821,15 +542,7 @@ async def aget_whats_app_broadcasts( ) return await self._client._aget("/v1/whatsapp/broadcasts", params=params) - async def acreate_whats_app_broadcast( - self, - account_id: str, - name: str, - template: dict[str, Any], - *, - description: str | None = None, - recipients: list[dict[str, Any]] | None = None, - ) -> dict[str, Any]: + async def acreate_whats_app_broadcast(self, account_id: str, name: str, template: dict[str, Any], *, description: str | None = None, recipients: list[dict[str, Any]] | None = None) -> dict[str, Any]: """Create broadcast (async)""" payload = self._build_payload( account_id=account_id, @@ -852,61 +565,36 @@ async def asend_whats_app_broadcast(self, broadcast_id: str) -> dict[str, Any]: """Send broadcast (async)""" return await self._client._apost(f"/v1/whatsapp/broadcasts/{broadcast_id}/send") - async def aschedule_whats_app_broadcast( - self, broadcast_id: str, scheduled_at: datetime | str - ) -> dict[str, Any]: + async def aschedule_whats_app_broadcast(self, broadcast_id: str, scheduled_at: datetime | str) -> dict[str, Any]: """Schedule broadcast (async)""" payload = self._build_payload( scheduled_at=scheduled_at, ) - return await self._client._apost( - f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule", data=payload - ) + return await self._client._apost(f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule", data=payload) - async def acancel_whats_app_broadcast_schedule( - self, broadcast_id: str - ) -> dict[str, Any]: + async def acancel_whats_app_broadcast_schedule(self, broadcast_id: str) -> dict[str, Any]: """Cancel scheduled broadcast (async)""" - return await self._client._adelete( - f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule" - ) - - async def aget_whats_app_broadcast_recipients( - self, - broadcast_id: str, - *, - status: str | None = None, - limit: int | None = 100, - skip: int | None = 0, - ) -> dict[str, Any]: + return await self._client._adelete(f"/v1/whatsapp/broadcasts/{broadcast_id}/schedule") + + async def aget_whats_app_broadcast_recipients(self, broadcast_id: str, *, status: str | None = None, limit: int | None = 100, skip: int | None = 0) -> dict[str, Any]: """List recipients (async)""" params = self._build_params( status=status, limit=limit, skip=skip, ) - return await self._client._aget( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", params=params - ) + return await self._client._aget(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", params=params) - async def aadd_whats_app_broadcast_recipients( - self, broadcast_id: str, recipients: list[dict[str, Any]] - ) -> dict[str, Any]: + async def aadd_whats_app_broadcast_recipients(self, broadcast_id: str, recipients: list[dict[str, Any]]) -> dict[str, Any]: """Add recipients (async)""" payload = self._build_payload( recipients=recipients, ) - return await self._client._apatch( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", data=payload - ) + return await self._client._apatch(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients", data=payload) - async def aremove_whats_app_broadcast_recipients( - self, broadcast_id: str, phones: list[str] - ) -> dict[str, Any]: + async def aremove_whats_app_broadcast_recipients(self, broadcast_id: str, phones: list[str]) -> dict[str, Any]: """Remove recipients (async)""" - return await self._client._adelete( - f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients" - ) + return await self._client._adelete(f"/v1/whatsapp/broadcasts/{broadcast_id}/recipients") async def aget_whats_app_business_profile(self, account_id: str) -> dict[str, Any]: """Get business profile (async)""" @@ -915,18 +603,7 @@ async def aget_whats_app_business_profile(self, account_id: str) -> dict[str, An ) return await self._client._aget("/v1/whatsapp/business-profile", params=params) - async def aupdate_whats_app_business_profile( - self, - account_id: str, - *, - about: str | None = None, - address: str | None = None, - description: str | None = None, - email: str | None = None, - websites: list[str] | None = None, - vertical: str | None = None, - profile_picture_handle: str | None = None, - ) -> dict[str, Any]: + async def aupdate_whats_app_business_profile(self, account_id: str, *, about: str | None = None, address: str | None = None, description: str | None = None, email: str | None = None, websites: list[str] | None = None, vertical: str | None = None, profile_picture_handle: str | None = None) -> dict[str, Any]: """Update business profile (async)""" payload = self._build_payload( account_id=account_id, @@ -949,25 +626,17 @@ async def aget_whats_app_display_name(self, account_id: str) -> dict[str, Any]: params = self._build_params( account_id=account_id, ) - return await self._client._aget( - "/v1/whatsapp/business-profile/display-name", params=params - ) + return await self._client._aget("/v1/whatsapp/business-profile/display-name", params=params) - async def aupdate_whats_app_display_name( - self, account_id: str, display_name: str - ) -> dict[str, Any]: + async def aupdate_whats_app_display_name(self, account_id: str, display_name: str) -> dict[str, Any]: """Request display name change (async)""" payload = self._build_payload( account_id=account_id, display_name=display_name, ) - return await self._client._apost( - "/v1/whatsapp/business-profile/display-name", data=payload - ) + return await self._client._apost("/v1/whatsapp/business-profile/display-name", data=payload) - async def alist_whats_app_group_chats( - self, account_id: str, *, limit: int | None = 25, after: str | None = None - ) -> dict[str, Any]: + async def alist_whats_app_group_chats(self, account_id: str, *, limit: int | None = 25, after: str | None = None) -> dict[str, Any]: """List active groups (async)""" params = self._build_params( account_id=account_id, @@ -976,14 +645,7 @@ async def alist_whats_app_group_chats( ) return await self._client._aget("/v1/whatsapp/wa-groups", params=params) - async def acreate_whats_app_group_chat( - self, - account_id: str, - subject: str, - *, - description: str | None = None, - join_approval_mode: str | None = None, - ) -> dict[str, Any]: + async def acreate_whats_app_group_chat(self, account_id: str, subject: str, *, description: str | None = None, join_approval_mode: str | None = None) -> dict[str, Any]: """Create group (async)""" payload = self._build_payload( account_id=account_id, @@ -993,109 +655,67 @@ async def acreate_whats_app_group_chat( ) return await self._client._apost("/v1/whatsapp/wa-groups", data=payload) - async def aget_whats_app_group_chat( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + async def aget_whats_app_group_chat(self, group_id: str, account_id: str) -> dict[str, Any]: """Get group info (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._aget( - f"/v1/whatsapp/wa-groups/{group_id}", params=params - ) + return await self._client._aget(f"/v1/whatsapp/wa-groups/{group_id}", params=params) - async def aupdate_whats_app_group_chat( - self, - group_id: str, - account_id: str, - *, - subject: str | None = None, - description: str | None = None, - join_approval_mode: str | None = None, - ) -> dict[str, Any]: + async def aupdate_whats_app_group_chat(self, group_id: str, account_id: str, *, subject: str | None = None, description: str | None = None, join_approval_mode: str | None = None) -> dict[str, Any]: """Update group settings (async)""" payload = self._build_payload( subject=subject, description=description, join_approval_mode=join_approval_mode, ) - return await self._client._apost( - f"/v1/whatsapp/wa-groups/{group_id}", data=payload - ) + return await self._client._apost(f"/v1/whatsapp/wa-groups/{group_id}", data=payload) - async def adelete_whats_app_group_chat( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + async def adelete_whats_app_group_chat(self, group_id: str, account_id: str) -> dict[str, Any]: """Delete group (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/whatsapp/wa-groups/{group_id}", params=params - ) + return await self._client._adelete(f"/v1/whatsapp/wa-groups/{group_id}", params=params) - async def aadd_whats_app_group_participants( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + async def aadd_whats_app_group_participants(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Add participants (async)""" payload = self._build_payload( phone_numbers=phone_numbers, ) - return await self._client._apost( - f"/v1/whatsapp/wa-groups/{group_id}/participants", data=payload - ) + return await self._client._apost(f"/v1/whatsapp/wa-groups/{group_id}/participants", data=payload) - async def aremove_whats_app_group_participants( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + async def aremove_whats_app_group_participants(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Remove participants (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/whatsapp/wa-groups/{group_id}/participants", params=params - ) + return await self._client._adelete(f"/v1/whatsapp/wa-groups/{group_id}/participants", params=params) - async def acreate_whats_app_group_invite_link( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + async def acreate_whats_app_group_invite_link(self, group_id: str, account_id: str) -> dict[str, Any]: """Create invite link (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._apost( - f"/v1/whatsapp/wa-groups/{group_id}/invite-link", params=params - ) + return await self._client._apost(f"/v1/whatsapp/wa-groups/{group_id}/invite-link", params=params) - async def alist_whats_app_group_join_requests( - self, group_id: str, account_id: str - ) -> dict[str, Any]: + async def alist_whats_app_group_join_requests(self, group_id: str, account_id: str) -> dict[str, Any]: """List join requests (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._aget( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params - ) + return await self._client._aget(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params) - async def aapprove_whats_app_group_join_requests( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + async def aapprove_whats_app_group_join_requests(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Approve join requests (async)""" payload = self._build_payload( phone_numbers=phone_numbers, ) - return await self._client._apost( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", data=payload - ) + return await self._client._apost(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", data=payload) - async def areject_whats_app_group_join_requests( - self, group_id: str, account_id: str, phone_numbers: list[str] - ) -> dict[str, Any]: + async def areject_whats_app_group_join_requests(self, group_id: str, account_id: str, phone_numbers: list[str]) -> dict[str, Any]: """Reject join requests (async)""" params = self._build_params( account_id=account_id, ) - return await self._client._adelete( - f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params - ) + return await self._client._adelete(f"/v1/whatsapp/wa-groups/{group_id}/join-requests", params=params) diff --git a/src/late/resources/_generated/whatsapp_phone_numbers.py b/src/late/resources/_generated/whatsapp_phone_numbers.py index 136d137..d248261 100644 --- a/src/late/resources/_generated/whatsapp_phone_numbers.py +++ b/src/late/resources/_generated/whatsapp_phone_numbers.py @@ -23,21 +23,17 @@ def __init__(self, client: BaseClient) -> None: def _build_params(self, **kwargs: Any) -> dict[str, Any]: """Build query parameters, filtering None values.""" - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - return {to_camel(k): v for k, v in kwargs.items() if v is not None} def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """Build request payload, filtering None values.""" from datetime import datetime - def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) - result: dict[str, Any] = {} for k, v in kwargs.items(): if v is None: @@ -48,9 +44,7 @@ def to_camel(s: str) -> str: result[to_camel(k)] = v return result - def get_whats_app_phone_numbers( - self, *, status: str | None = None, profile_id: str | None = None - ) -> dict[str, Any]: + def get_whats_app_phone_numbers(self, *, status: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """List phone numbers""" params = self._build_params( status=status, @@ -73,9 +67,7 @@ def release_whats_app_phone_number(self, phone_number_id: str) -> dict[str, Any] """Release phone number""" return self._client._delete(f"/v1/whatsapp/phone-numbers/{phone_number_id}") - async def aget_whats_app_phone_numbers( - self, *, status: str | None = None, profile_id: str | None = None - ) -> dict[str, Any]: + async def aget_whats_app_phone_numbers(self, *, status: str | None = None, profile_id: str | None = None) -> dict[str, Any]: """List phone numbers (async)""" params = self._build_params( status=status, @@ -88,18 +80,12 @@ async def apurchase_whats_app_phone_number(self, profile_id: str) -> dict[str, A payload = self._build_payload( profile_id=profile_id, ) - return await self._client._apost( - "/v1/whatsapp/phone-numbers/purchase", data=payload - ) + return await self._client._apost("/v1/whatsapp/phone-numbers/purchase", data=payload) async def aget_whats_app_phone_number(self, phone_number_id: str) -> dict[str, Any]: """Get phone number (async)""" return await self._client._aget(f"/v1/whatsapp/phone-numbers/{phone_number_id}") - async def arelease_whats_app_phone_number( - self, phone_number_id: str - ) -> dict[str, Any]: + async def arelease_whats_app_phone_number(self, phone_number_id: str) -> dict[str, Any]: """Release phone number (async)""" - return await self._client._adelete( - f"/v1/whatsapp/phone-numbers/{phone_number_id}" - ) + return await self._client._adelete(f"/v1/whatsapp/phone-numbers/{phone_number_id}") From 0dc65d61de8e5dd4841921d0e82f6e4d6d67f0f5 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:18:32 +0200 Subject: [PATCH 16/17] fix: sort imports by source group to satisfy ruff linter Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/generate_resources.py | 20 ++++++++++++++------ src/late/resources/__init__.py | 17 +++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/scripts/generate_resources.py b/scripts/generate_resources.py index 6cbea26..88d8d2b 100644 --- a/scripts/generate_resources.py +++ b/scripts/generate_resources.py @@ -435,13 +435,21 @@ def generate_init_file(generated_resources: list[str], manual_resources: list[st all_resources = sorted(set(generated_resources) | set(manual_resources)) - # Imports: prefer manual over auto-generated - for resource in all_resources: + # Split into auto-generated and manual groups (ruff requires sorted import blocks + # grouped by source: ._generated first, then local .) + auto_only = sorted(r for r in all_resources if r not in manual_resources) + manual_only = sorted(r for r in all_resources if r in manual_resources) + + for resource in auto_only: class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - if resource in manual_resources: - lines.append(f"from .{resource} import {class_name}") - else: - lines.append(f"from ._generated.{resource} import {class_name}") + lines.append(f"from ._generated.{resource} import {class_name}") + + if auto_only and manual_only: + lines.append("") # blank line between import groups + + for resource in manual_only: + class_name = "".join(word.title() for word in resource.split("_")) + "Resource" + lines.append(f"from .{resource} import {class_name}") lines.append("") lines.append("__all__ = [") diff --git a/src/late/resources/__init__.py b/src/late/resources/__init__.py index f9a1dc2..c23f47b 100644 --- a/src/late/resources/__init__.py +++ b/src/late/resources/__init__.py @@ -8,11 +8,9 @@ from ._generated.account_groups import AccountGroupsResource from ._generated.account_settings import AccountSettingsResource -from .accounts import AccountsResource from ._generated.ad_audiences import AdAudiencesResource from ._generated.ad_campaigns import AdCampaignsResource from ._generated.ads import AdsResource -from .analytics import AnalyticsResource from ._generated.api_keys import ApiKeysResource from ._generated.broadcasts import BroadcastsResource from ._generated.comment_automations import CommentAutomationsResource @@ -22,23 +20,26 @@ from ._generated.custom_fields import CustomFieldsResource from ._generated.invites import InvitesResource from ._generated.logs import LogsResource -from .media import MediaResource from ._generated.messages import MessagesResource -from .posts import PostsResource -from .profiles import ProfilesResource -from .queue import QueueResource from ._generated.reddit import RedditResource from ._generated.reviews import ReviewsResource from ._generated.sequences import SequencesResource -from .tools import ToolsResource from ._generated.twitter_engagement import TwitterEngagementResource from ._generated.usage import UsageResource -from .users import UsersResource from ._generated.validate import ValidateResource from ._generated.webhooks import WebhooksResource from ._generated.whatsapp import WhatsappResource from ._generated.whatsapp_phone_numbers import WhatsappPhoneNumbersResource +from .accounts import AccountsResource +from .analytics import AnalyticsResource +from .media import MediaResource +from .posts import PostsResource +from .profiles import ProfilesResource +from .queue import QueueResource +from .tools import ToolsResource +from .users import UsersResource + __all__ = [ "AccountGroupsResource", "AccountSettingsResource", From 71e21813ca404c5450eaa4d589cae9f20449f6f7 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:21:14 +0200 Subject: [PATCH 17/17] fix: sort generated imports to match ruff isort rules Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/generate_resources.py | 35 +++++++++++++++++----------------- src/late/client/late_client.py | 2 +- src/late/resources/__init__.py | 1 - 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/scripts/generate_resources.py b/scripts/generate_resources.py index 88d8d2b..f5c141d 100644 --- a/scripts/generate_resources.py +++ b/scripts/generate_resources.py @@ -435,21 +435,20 @@ def generate_init_file(generated_resources: list[str], manual_resources: list[st all_resources = sorted(set(generated_resources) | set(manual_resources)) - # Split into auto-generated and manual groups (ruff requires sorted import blocks - # grouped by source: ._generated first, then local .) - auto_only = sorted(r for r in all_resources if r not in manual_resources) - manual_only = sorted(r for r in all_resources if r in manual_resources) - - for resource in auto_only: + # Build import lines sorted by full module path to match ruff isort. + # ruff sorts all relative imports as one block by module path: + # ._generated.x comes before .x (since '_' < 'a' in ASCII). + import_lines: list[tuple[str, str]] = [] + for resource in all_resources: class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - lines.append(f"from ._generated.{resource} import {class_name}") - - if auto_only and manual_only: - lines.append("") # blank line between import groups + if resource in manual_resources: + import_lines.append((f".{resource}", f"from .{resource} import {class_name}")) + else: + import_lines.append((f"._generated.{resource}", f"from ._generated.{resource} import {class_name}")) - for resource in manual_only: - class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - lines.append(f"from .{resource} import {class_name}") + import_lines.sort(key=lambda x: x[0]) + for _, line in import_lines: + lines.append(line) lines.append("") lines.append("__all__ = [") @@ -469,11 +468,11 @@ def update_client_file(client_path: Path, all_resources: list[str]) -> None: """ content = client_path.read_text() - # Build new import block - class_names = [] - for resource in sorted(all_resources): - class_name = "".join(word.title() for word in resource.split("_")) + "Resource" - class_names.append(class_name) + # Build new import block, sorted by class name to match ruff isort + class_names = sorted( + "".join(word.title() for word in r.split("_")) + "Resource" + for r in all_resources + ) import_block = "from ..resources import (\n" for cn in class_names: diff --git a/src/late/client/late_client.py b/src/late/client/late_client.py index c33a08e..98c0419 100644 --- a/src/late/client/late_client.py +++ b/src/late/client/late_client.py @@ -39,8 +39,8 @@ UsersResource, ValidateResource, WebhooksResource, - WhatsappResource, WhatsappPhoneNumbersResource, + WhatsappResource, ) from .base import BaseClient diff --git a/src/late/resources/__init__.py b/src/late/resources/__init__.py index c23f47b..148dab3 100644 --- a/src/late/resources/__init__.py +++ b/src/late/resources/__init__.py @@ -30,7 +30,6 @@ from ._generated.webhooks import WebhooksResource from ._generated.whatsapp import WhatsappResource from ._generated.whatsapp_phone_numbers import WhatsappPhoneNumbersResource - from .accounts import AccountsResource from .analytics import AnalyticsResource from .media import MediaResource