From 33632d9b4a47abf6b4ed93df31c83e1b94c99a21 Mon Sep 17 00:00:00 2001 From: Oscar Perez Date: Fri, 31 Jul 2026 11:01:43 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20make=20failed-post=20retry=20work=20?= =?UTF-8?q?=E2=80=94=20enum=20status=20mismatch=20(zernio-claude-plugin#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP retry path never recognized a failed post. Two bugs conspired: 1. posts_retry compared the post's status — a generated plain Enum with an unstable positional name (Status10 in the released build, renumbered on every spec regen) — against the handwritten PostStatus str-enum. Two different enum classes never compare equal, so every failed post was rejected with 'is not in failed status (current: Status10.FAILED)'. 2. posts_retry_all_failed / posts_list_failed passed PostStatus.FAILED into the query string, where httpx serialized it via str() as 'status=PostStatus.FAILED' instead of 'status=failed', so the API matched nothing and the tools reported no failed posts. Fixes: - codegen: add --use-subclass-enum so generated enums are (str, Enum) and compare equal to their plain value and to the PostStatus twin (models regenerated). - resources: unwrap Enum members to .value in _build_params/_build_payload — in the generator template (each generated resource carries its own copy that shadows BaseResource) and in the handwritten BaseResource (resources regenerated). - mcp: posts_retry guard compares plain status values and reports the clean value ('current: scheduled') in the warning. - tests: unit tests pin enum unwrapping and value-compatibility; test_mcp_retry_regression.py drives the real MCP tools against a strict in-memory fake API (httpx.MockTransport, no network) that only matches the literal status=failed — both bugs reproduce exactly against the unfixed code with the same error messages as the issue. Co-Authored-By: Claude Fable 5 --- scripts/generate_models.py | 1 + scripts/generate_resources.py | 23 +- src/late/client/late_client.py | 4 +- src/late/mcp/server.py | 5 +- src/late/models/_generated/models.py | 306 +++++++++--------- .../resources/_generated/account_groups.py | 23 +- .../resources/_generated/account_settings.py | 23 +- src/late/resources/_generated/accounts.py | 23 +- src/late/resources/_generated/ad_accounts.py | 23 +- src/late/resources/_generated/ad_audiences.py | 23 +- src/late/resources/_generated/ad_campaigns.py | 23 +- src/late/resources/_generated/ad_creatives.py | 23 +- src/late/resources/_generated/ad_insights.py | 23 +- src/late/resources/_generated/ad_targeting.py | 23 +- src/late/resources/_generated/analytics.py | 23 +- src/late/resources/_generated/api_keys.py | 23 +- src/late/resources/_generated/broadcasts.py | 23 +- src/late/resources/_generated/calls.py | 23 +- .../_generated/comment_automations.py | 23 +- src/late/resources/_generated/comments.py | 23 +- src/late/resources/_generated/connect.py | 23 +- .../resources/_generated/connected_apps.py | 23 +- src/late/resources/_generated/contacts.py | 23 +- src/late/resources/_generated/conversions.py | 23 +- .../resources/_generated/custom_fields.py | 23 +- src/late/resources/_generated/discord.py | 23 +- src/late/resources/_generated/gmb_services.py | 23 +- .../resources/_generated/gmb_verifications.py | 23 +- .../resources/_generated/inbox_analytics.py | 23 +- src/late/resources/_generated/instagram.py | 23 +- src/late/resources/_generated/invites.py | 23 +- src/late/resources/_generated/lead_gen.py | 23 +- src/late/resources/_generated/logs.py | 23 +- src/late/resources/_generated/media.py | 23 +- src/late/resources/_generated/mentions.py | 23 +- src/late/resources/_generated/messages.py | 23 +- .../resources/_generated/messaging_ads.py | 23 +- .../resources/_generated/phone_numbers.py | 23 +- src/late/resources/_generated/posts.py | 23 +- src/late/resources/_generated/profiles.py | 23 +- src/late/resources/_generated/queue.py | 23 +- .../_generated/reach_and_frequency.py | 23 +- src/late/resources/_generated/reddit.py | 23 +- src/late/resources/_generated/reviews.py | 23 +- src/late/resources/_generated/sequences.py | 23 +- src/late/resources/_generated/sms.py | 23 +- .../resources/_generated/tracking_tags.py | 23 +- .../_generated/twitter_engagement.py | 23 +- src/late/resources/_generated/usage.py | 23 +- src/late/resources/_generated/users.py | 23 +- src/late/resources/_generated/validate.py | 23 +- src/late/resources/_generated/verify.py | 23 +- src/late/resources/_generated/voice.py | 23 +- src/late/resources/_generated/webhooks.py | 23 +- src/late/resources/_generated/whatsapp.py | 23 +- .../resources/_generated/whatsapp_calling.py | 23 +- .../resources/_generated/whatsapp_flows.py | 23 +- .../_generated/whatsapp_phone_numbers.py | 23 +- .../resources/_generated/whatsapp_sandbox.py | 23 +- .../_generated/whatsapp_templates.py | 23 +- src/late/resources/_generated/workflows.py | 23 +- src/late/resources/base.py | 23 +- tests/test_integration.py | 50 +++ tests/test_mcp_retry_regression.py | 153 +++++++++ 64 files changed, 1580 insertions(+), 273 deletions(-) create mode 100644 tests/test_mcp_retry_regression.py diff --git a/scripts/generate_models.py b/scripts/generate_models.py index fb12626..81a9910 100644 --- a/scripts/generate_models.py +++ b/scripts/generate_models.py @@ -56,6 +56,7 @@ def main() -> int: "--field-constraints", "--use-field-description", "--capitalise-enum-members", + "--use-subclass-enum", "--use-default-kwarg", "--collapse-root-models", "--use-union-operator", diff --git a/scripts/generate_resources.py b/scripts/generate_resources.py index efde8e2..2908d11 100644 --- a/scripts/generate_resources.py +++ b/scripts/generate_resources.py @@ -394,20 +394,39 @@ def generate_resource_class( " default for optional string args, and the API rejects empty query", " values (e.g. ``platform=``) with a 400. Filtering here keeps both direct", " SDK callers and MCP tool callers safe.", + "", + " Enum members are unwrapped to their value: httpx serializes params", + ' via str(), which yields "ClassName.MEMBER" for Enum members, so', + " e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as", + " ``status=PostStatus.FAILED`` instead of ``status=failed``.", ' """', + " from enum import Enum", " 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 and v != ""}', + " result: dict[str, Any] = {}", + " for k, v in kwargs.items():", + " if isinstance(v, Enum):", + " v = v.value", + ' if v is None or v == "":', + " continue", + " result[to_camel(k)] = v", + " return result", "", " def _build_payload(self, **kwargs: Any) -> dict[str, Any]:", - ' """Build request payload, filtering None values."""', + ' """Build request payload, filtering None values. Enum members are', + ' unwrapped to their value so JSON bodies carry e.g. "failed" rather', + " than a raw Enum member (plain Enum members are not JSON-serializable).", + ' """', " from datetime import datetime", + " from enum import Enum", " 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 isinstance(v, Enum):", + " v = v.value", " if v is None:", " continue", " if isinstance(v, datetime):", diff --git a/src/late/client/late_client.py b/src/late/client/late_client.py index bdcb206..21d4bdf 100644 --- a/src/late/client/late_client.py +++ b/src/late/client/late_client.py @@ -17,16 +17,16 @@ AdCampaignsResource, AdCreativesResource, AdInsightsResource, - AdTargetingResource, AdsResource, + AdTargetingResource, AnalyticsResource, ApiKeysResource, BroadcastsResource, CallsResource, CommentAutomationsResource, CommentsResource, - ConnectResource, ConnectedAppsResource, + ConnectResource, ContactsResource, ConversionsResource, CustomFieldsResource, diff --git a/src/late/mcp/server.py b/src/late/mcp/server.py index f619b3a..982dc2d 100644 --- a/src/late/mcp/server.py +++ b/src/late/mcp/server.py @@ -756,8 +756,9 @@ def posts_retry(post_id: str) -> str: post = post_response.post if not post: return f"\u274c Post {post_id} not found" - if post.status != PostStatus.FAILED: - return f"\u26a0\ufe0f Post {post_id} is not in failed status (current: {post.status})" + status_value = post.status.value if post.status else "unknown" + if status_value != PostStatus.FAILED.value: + return f"\u26a0\ufe0f Post {post_id} is not in failed status (current: {status_value})" except Exception as e: return f"\u274c Could not find post {post_id}: {e}" diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index e28b14a..4ce282d 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-07-31T14:21:24+00:00 +# timestamp: 2026-07-31T14:28:31+00:00 from __future__ import annotations @@ -11,7 +11,7 @@ from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field, RootModel -class Status(Enum): +class Status(str, Enum): PENDING = "pending" APPROVED = "approved" EXPIRED = "expired" @@ -20,7 +20,7 @@ class Status(Enum): DELIVERY_FAILED = "delivery_failed" -class Channel(Enum): +class Channel(str, Enum): SMS = "sms" @@ -145,7 +145,7 @@ class Creative(BaseModel): """ -class BudgetType(Enum): +class BudgetType(str, Enum): DAILY = "daily" LIFETIME = "lifetime" @@ -154,7 +154,7 @@ class Country(RootModel[str]): root: Annotated[str, Field(max_length=2, min_length=2)] -class DistanceUnit(Enum): +class DistanceUnit(str, Enum): MILE = "mile" KILOMETER = "kilometer" @@ -193,7 +193,7 @@ class Interest(BaseModel): name: str | None = None -class PublisherPlatform(Enum): +class PublisherPlatform(str, Enum): FACEBOOK = "facebook" INSTAGRAM = "instagram" THREADS = "threads" @@ -202,7 +202,7 @@ class PublisherPlatform(Enum): WHATSAPP = "whatsapp" -class FacebookPosition(Enum): +class FacebookPosition(str, Enum): FEED = "feed" RIGHT_HAND_COLUMN = "right_hand_column" MARKETPLACE = "marketplace" @@ -216,7 +216,7 @@ class FacebookPosition(Enum): NOTIFICATION = "notification" -class InstagramPosition(Enum): +class InstagramPosition(str, Enum): STREAM = "stream" STORY = "story" EXPLORE = "explore" @@ -227,26 +227,26 @@ class InstagramPosition(Enum): PROFILE_REELS = "profile_reels" -class MessengerPosition(Enum): +class MessengerPosition(str, Enum): MESSENGER_HOME = "messenger_home" SPONSORED_MESSAGES = "sponsored_messages" STORY = "story" -class AudienceNetworkPosition(Enum): +class AudienceNetworkPosition(str, Enum): CLASSIC = "classic" REWARDED_VIDEO = "rewarded_video" -class ThreadsPosition(Enum): +class ThreadsPosition(str, Enum): THREADS_STREAM = "threads_stream" -class WhatsappPosition(Enum): +class WhatsappPosition(str, Enum): STATUS = "status" -class DevicePlatform(Enum): +class DevicePlatform(str, Enum): MOBILE = "mobile" DESKTOP = "desktop" @@ -293,7 +293,7 @@ class AdvantageAudience(IntEnum): INTEGER_1 = 1 -class Objective(Enum): +class Objective(str, Enum): """ Defaults to `OUTCOME_ENGAGEMENT`. `OUTCOME_SALES` and `OUTCOME_LEADS` require additional account configuration (Dataset linked to the WABA @@ -306,7 +306,7 @@ class Objective(Enum): OUTCOME_LEADS = "OUTCOME_LEADS" -class BidStrategy(Enum): +class BidStrategy(str, Enum): """ Meta bid strategy applied to the shared ad set. Defaults to `LOWEST_COST_WITHOUT_CAP` (auto-bid) when omitted. @@ -530,7 +530,7 @@ class CtwaAdRequestBody(BaseModel): """ -class Type(Enum): +class Type(str, Enum): """ Node kind. The 16 supported types break into four groups: messaging (send_message), @@ -656,7 +656,7 @@ class WorkflowEdge(BaseModel): """ -class Action(Enum): +class Action(str, Enum): EXECUTION_STARTED = "execution_started" EXECUTION_COMPLETED = "execution_completed" EXECUTION_EXITED = "execution_exited" @@ -919,7 +919,7 @@ class ExternalPostSummary(BaseModel): """ -class Type1(Enum): +class Type1(str, Enum): """ Error class for programmatic handling. """ @@ -976,7 +976,7 @@ class ErrorResponse(BaseModel): """ -class Type2(Enum): +class Type2(str, Enum): URL = "url" POSTBACK = "postback" PHONE = "phone" @@ -1010,7 +1010,7 @@ class DmButton(BaseModel): """ -class Type3(Enum): +class Type3(str, Enum): QUICK_REPLY = "quick_reply" URL = "url" PHONE_NUMBER = "phone_number" @@ -1021,7 +1021,7 @@ class Type3(Enum): CATALOG = "catalog" -class OtpType(Enum): +class OtpType(str, Enum): """ Required when type is otp """ @@ -1063,11 +1063,11 @@ class WhatsAppTemplateButton(BaseModel): navigate_screen: str | None = None -class Type4(Enum): +class Type4(str, Enum): HEADER = "header" -class Format(Enum): +class Format(str, Enum): TEXT = "text" IMAGE = "image" VIDEO = "video" @@ -1097,7 +1097,7 @@ class WhatsAppHeaderComponent(BaseModel): example: Example | None = None -class Type5(Enum): +class Type5(str, Enum): BODY = "body" @@ -1121,7 +1121,7 @@ class WhatsAppBodyComponent(BaseModel): example: Example1 | None = None -class Type6(Enum): +class Type6(str, Enum): FOOTER = "footer" @@ -1137,7 +1137,7 @@ class WhatsAppFooterComponent(BaseModel): """ -class Type7(Enum): +class Type7(str, Enum): BUTTONS = "buttons" @@ -1146,11 +1146,11 @@ class WhatsAppButtonsComponent(BaseModel): buttons: Annotated[List[WhatsAppTemplateButton], Field(min_length=1)] -class Type8(Enum): +class Type8(str, Enum): CAROUSEL = "carousel" -class Type9(Enum): +class Type9(str, Enum): LIMITED_TIME_OFFER = "limited_time_offer" @@ -1170,7 +1170,7 @@ class WhatsAppLimitedTimeOfferComponent(BaseModel): limited_time_offer: LimitedTimeOffer -class Status2(Enum): +class Status2(str, Enum): """ `pending` until the phone replies to the activation template, then `active`. Expired sessions are pruned by TTL and never appear in @@ -1248,7 +1248,7 @@ class Money(BaseModel): """ -class Spiciness(Enum): +class Spiciness(str, Enum): """ Spiciness level (e.g. MILD, MEDIUM, HOT) """ @@ -1464,7 +1464,7 @@ class YouTubeScopeMissingResponse(BaseModel): scopeStatus: ScopeStatus2 | None = None -class Platform(Enum): +class Platform(str, Enum): """ Platform that served this response. """ @@ -1481,7 +1481,7 @@ class DateRange2(BaseModel): until: date_aliased | None = None -class MetricType(Enum): +class MetricType(str, Enum): TIME_SERIES = "time_series" TOTAL_VALUE = "total_value" @@ -1496,7 +1496,7 @@ class Breakdown(BaseModel): value: float | None = None -class Unit(Enum): +class Unit(str, Enum): """ Present on monetary metrics only. The scale of "total" and of every "values[].value", exactly as the platform returned them. @@ -1555,7 +1555,7 @@ class Metrics(BaseModel): """ -class Reason(Enum): +class Reason(str, Enum): """ "not_enrolled": the account is not enrolled in the program behind this metric. "permission_missing": the connected user lacks access to this metric. @@ -1660,7 +1660,7 @@ class InstagramAccountInsightsResponse(BaseModel): ] = None -class Period(Enum): +class Period(str, Enum): """ Always "lifetime": the total is cumulative since publication and must not be summed across dates or across posts. @@ -1670,7 +1670,7 @@ class Period(Enum): LIFETIME = "lifetime" -class Unit1(Enum): +class Unit1(str, Enum): """ "micro_amount": Meta returned an object shape carrying a micro amount, and "total" is that integer, unconverted. Zernio does not publish a divisor because Meta does not @@ -1712,7 +1712,7 @@ class Metrics1(BaseModel): """ -class Reason1(Enum): +class Reason1(str, Enum): """ "not_enrolled": the account is not enrolled in the program behind this metric. "permission_missing": the connected user lacks access to this metric. @@ -1798,12 +1798,12 @@ class FacebookPostEarningsResponse(BaseModel): dataDelay: str | None = None -class Metric(Enum): +class Metric(str, Enum): FOLLOWER_DEMOGRAPHICS = "follower_demographics" ENGAGED_AUDIENCE_DEMOGRAPHICS = "engaged_audience_demographics" -class Timeframe(Enum): +class Timeframe(str, Enum): """ The timeframe used for demographic data """ @@ -1903,7 +1903,7 @@ class YouTubeDemographicsResponse(BaseModel): ] = None -class Event(Enum): +class Event(str, Enum): POST_SCHEDULED = "post.scheduled" POST_PUBLISHED = "post.published" POST_FAILED = "post.failed" @@ -1995,7 +1995,7 @@ class Webhook(BaseModel): """ -class Status3(Enum): +class Status3(str, Enum): """ Delivery outcome """ @@ -2067,7 +2067,7 @@ class WebhookLog(BaseModel): """ -class Event1(Enum): +class Event1(str, Enum): POST_SCHEDULED = "post.scheduled" POST_PUBLISHED = "post.published" POST_FAILED = "post.failed" @@ -2115,7 +2115,7 @@ class WebhookPayloadPost(BaseModel): timestamp: AwareDatetime -class Event2(Enum): +class Event2(str, Enum): POST_PLATFORM_PUBLISHED = "post.platform.published" POST_PLATFORM_FAILED = "post.platform.failed" POST_PLATFORM_DELETED = "post.platform.deleted" @@ -2153,7 +2153,7 @@ class Post1(BaseModel): """ -class Status4(Enum): +class Status4(str, Enum): """ Terminal status this event fires on. Matches the event suffix. """ @@ -2241,7 +2241,7 @@ class WebhookPayloadPostPlatform(BaseModel): timestamp: AwareDatetime -class Event3(Enum): +class Event3(str, Enum): ACCOUNT_CONNECTED = "account.connected" @@ -2273,11 +2273,11 @@ class WebhookPayloadAccountConnected(BaseModel): timestamp: AwareDatetime -class Event4(Enum): +class Event4(str, Enum): ACCOUNT_DISCONNECTED = "account.disconnected" -class DisconnectionType(Enum): +class DisconnectionType(str, Enum): """ Whether the disconnection was intentional (user action) or unintentional (token expired/revoked) """ @@ -2322,7 +2322,7 @@ class WebhookPayloadAccountDisconnected(BaseModel): timestamp: AwareDatetime -class Event5(Enum): +class Event5(str, Enum): ACCOUNT_ADS_INITIAL_SYNC_COMPLETED = "account.ads.initial_sync_completed" @@ -2367,7 +2367,7 @@ class Account3(BaseModel): """ -class Status5(Enum): +class Status5(str, Enum): """ Overall outcome of the initial sync. """ @@ -2376,7 +2376,7 @@ class Status5(Enum): FAILURE = "failure" -class ErrorCategory(Enum): +class ErrorCategory(str, Enum): """ Stable category for UX branching. New values may be added; existing ones are stable. Mapping: @@ -2475,11 +2475,11 @@ class WebhookPayloadAccountAdsInitialSyncCompleted(BaseModel): timestamp: AwareDatetime -class Event6(Enum): +class Event6(str, Enum): COMMENT_RECEIVED = "comment.received" -class Platform4(Enum): +class Platform4(str, Enum): INSTAGRAM = "instagram" FACEBOOK = "facebook" TWITTER = "twitter" @@ -2632,7 +2632,7 @@ class WebhookPayloadComment(BaseModel): timestamp: AwareDatetime -class Event7(Enum): +class Event7(str, Enum): LEAD_RECEIVED = "lead.received" @@ -2674,7 +2674,7 @@ class Lead(BaseModel): """ -class Platform5(Enum): +class Platform5(str, Enum): FACEBOOK = "facebook" @@ -2705,7 +2705,7 @@ class WebhookPayloadLead(BaseModel): timestamp: AwareDatetime -class Platform6(Enum): +class Platform6(str, Enum): """ Platform the review originated on. Currently Google Business Profile only. """ @@ -2764,7 +2764,7 @@ class ReviewWebhookReview(BaseModel): """ -class Event8(Enum): +class Event8(str, Enum): REVIEW_NEW = "review.new" @@ -2793,7 +2793,7 @@ class WebhookPayloadReviewNew(BaseModel): timestamp: AwareDatetime -class Event9(Enum): +class Event9(str, Enum): REVIEW_UPDATED = "review.updated" @@ -2816,7 +2816,7 @@ class WebhookPayloadReviewUpdated(BaseModel): timestamp: AwareDatetime -class Source(Enum): +class Source(str, Enum): """ Always "external" — distinguishes these from Zernio-originated post.* events. """ @@ -2824,20 +2824,20 @@ class Source(Enum): EXTERNAL = "external" -class Event10(Enum): +class Event10(str, Enum): POST_EXTERNAL_CREATED = "post.external.created" POST_EXTERNAL_UPDATED = "post.external.updated" POST_EXTERNAL_DELETED = "post.external.deleted" -class Platform7(Enum): +class Platform7(str, Enum): INSTAGRAM = "instagram" FACEBOOK = "facebook" TELEGRAM = "telegram" WHATSAPP = "whatsapp" -class Direction(Enum): +class Direction(str, Enum): INCOMING = "incoming" OUTGOING = "outgoing" @@ -2962,7 +2962,7 @@ class InboxWebhookMessage(BaseModel): isRead: bool -class Status6(Enum): +class Status6(str, Enum): ACTIVE = "active" ARCHIVED = "archived" @@ -3013,11 +3013,11 @@ class InboxWebhookAccount(BaseModel): displayName: str | None = None -class Event11(Enum): +class Event11(str, Enum): REACTION_RECEIVED = "reaction.received" -class Action1(Enum): +class Action1(str, Enum): ADDED = "added" REMOVED = "removed" @@ -3080,7 +3080,7 @@ class WebhookPayloadReaction(BaseModel): timestamp: AwareDatetime -class Event12(Enum): +class Event12(str, Enum): MESSAGE_RECEIVED = "message.received" @@ -3214,7 +3214,7 @@ class Message(BaseModel): isRead: bool -class InteractiveType(Enum): +class InteractiveType(str, Enum): """ WhatsApp only. Which kind of interactive reply the user sent: `button_reply` (tap on an interactive button), `list_reply` (tap on a @@ -3500,7 +3500,7 @@ class WebhookPayloadMessage(BaseModel): timestamp: AwareDatetime -class Event13(Enum): +class Event13(str, Enum): MESSAGE_SENT = "message.sent" @@ -3536,7 +3536,7 @@ class Sender3(BaseModel): picture: str | None = None -class Source1(Enum): +class Source1(str, Enum): """ WhatsApp send origin. whatsapp_business_app when sent from the WhatsApp Business phone app on a Coexistence number; cloud_api when sent through Zernio (dashboard, API, or broadcasts). Absent on non-WhatsApp platforms. This is not the inbox metadata.source lineage field. """ @@ -3610,7 +3610,7 @@ class InboxMessageEditHistoryEntry(BaseModel): editedAt: AwareDatetime -class Event14(Enum): +class Event14(str, Enum): MESSAGE_EDITED = "message.edited" @@ -3645,7 +3645,7 @@ class WebhookPayloadMessageEdited(BaseModel): timestamp: AwareDatetime -class Event15(Enum): +class Event15(str, Enum): MESSAGE_DELETED = "message.deleted" @@ -3672,7 +3672,7 @@ class WebhookPayloadMessageDeleted(BaseModel): timestamp: AwareDatetime -class Event16(Enum): +class Event16(str, Enum): MESSAGE_DELIVERED = "message.delivered" MESSAGE_READ = "message.read" MESSAGE_FAILED = "message.failed" @@ -3726,11 +3726,11 @@ class WebhookPayloadMessageDeliveryStatus(BaseModel): timestamp: AwareDatetime -class Event17(Enum): +class Event17(str, Enum): CONVERSATION_STARTED = "conversation.started" -class Platform10(Enum): +class Platform10(str, Enum): INSTAGRAM = "instagram" FACEBOOK = "facebook" TELEGRAM = "telegram" @@ -3795,24 +3795,24 @@ class WebhookPayloadConversationStarted(BaseModel): timestamp: AwareDatetime -class Channel1(Enum): +class Channel1(str, Enum): WHATSAPP = "whatsapp" PSTN = "pstn" -class Direction3(Enum): +class Direction3(str, Enum): INBOUND = "inbound" OUTBOUND = "outbound" -class Status8(Enum): +class Status8(str, Enum): RINGING = "ringing" ANSWERED = "answered" ENDED = "ended" FAILED = "failed" -class ForwardCallerId(Enum): +class ForwardCallerId(str, Enum): """ Caller ID presented on the forwarded leg. """ @@ -3821,13 +3821,13 @@ class ForwardCallerId(Enum): CALLER = "caller" -class TranscriptionLanguage(Enum): +class TranscriptionLanguage(str, Enum): AUTO = "auto" EN = "en" ES = "es" -class EndReason(Enum): +class EndReason(str, Enum): HANGUP = "hangup" NO_ANSWER = "no_answer" REJECTED = "rejected" @@ -3968,7 +3968,7 @@ class CallRecord(BaseModel): updatedAt: AwareDatetime | None = None -class Event18(Enum): +class Event18(str, Enum): CALL_RECEIVED = "call.received" @@ -4022,7 +4022,7 @@ class WebhookPayloadCallReceived(BaseModel): timestamp: AwareDatetime -class Event19(Enum): +class Event19(str, Enum): CALL_ENDED = "call.ended" @@ -4074,7 +4074,7 @@ class WebhookPayloadCallEnded(BaseModel): timestamp: AwareDatetime -class Event20(Enum): +class Event20(str, Enum): CALL_FAILED = "call.failed" @@ -4109,11 +4109,11 @@ class WebhookPayloadCallFailed(BaseModel): timestamp: AwareDatetime -class Event21(Enum): +class Event21(str, Enum): CALL_PERMISSION_REQUEST = "call.permission_request" -class Response(Enum): +class Response(str, Enum): ACCEPT = "accept" REJECT = "reject" @@ -4150,7 +4150,7 @@ class WebhookPayloadCallPermissionRequest(BaseModel): timestamp: AwareDatetime -class Event22(Enum): +class Event22(str, Enum): AD_STATUS_CHANGED = "ad.status_changed" @@ -4181,7 +4181,7 @@ class Account9(BaseModel): """ -class Level(Enum): +class Level(str, Enum): """ Hierarchy level the status applies to. Mirrors Meta's `level`. Creative-level events are not forwarded. """ @@ -4307,11 +4307,11 @@ class WebhookPayloadAdStatusChanged(BaseModel): """ -class Event23(Enum): +class Event23(str, Enum): WHATSAPP_TEMPLATE_STATUS_UPDATED = "whatsapp.template.status_updated" -class Platform11(Enum): +class Platform11(str, Enum): WHATSAPP = "whatsapp" @@ -4323,7 +4323,7 @@ class Account10(BaseModel): displayName: str | None = None -class Status10(Enum): +class Status10(str, Enum): """ New status. Forwarded verbatim from Meta's `event` field. `PENDING_DELETION` is the 24h-grace state after a delete @@ -4390,7 +4390,7 @@ class WebhookPayloadWhatsAppTemplateStatusUpdated(BaseModel): """ -class Event24(Enum): +class Event24(str, Enum): WEBHOOK_TEST = "webhook.test" @@ -4499,7 +4499,7 @@ class GeoRestriction(BaseModel): """ -class Type10(Enum): +class Type10(str, Enum): IMAGE = "image" VIDEO = "video" GIF = "gif" @@ -4544,7 +4544,7 @@ class MediaItem(BaseModel): """ -class Type11(Enum): +class Type11(str, Enum): IMAGE = "image" VIDEO = "video" @@ -4560,7 +4560,7 @@ class ExternalPostMediaItem(BaseModel): thumbnail: str | None = None -class TrialGraduationStrategy(Enum): +class TrialGraduationStrategy(str, Enum): """ Graduation strategy the trial reel was launched with. Present only when isTrialReel is true. """ @@ -4569,7 +4569,7 @@ class TrialGraduationStrategy(Enum): SS_PERFORMANCE = "SS_PERFORMANCE" -class ErrorCategory1(Enum): +class ErrorCategory1(str, Enum): """ Error category for programmatic handling: auth_expired (token expired/revoked), user_content (wrong format/too long), user_abuse (rate limits/spam), account_issue (config problems), platform_rejected (policy violation), platform_error (5xx/maintenance), system_error (Zernio infra), unknown """ @@ -4584,7 +4584,7 @@ class ErrorCategory1(Enum): UNKNOWN = "unknown" -class ErrorSource(Enum): +class ErrorSource(str, Enum): """ Who caused the error: user (fix content/reconnect), platform (outage/API change), system (Zernio issue, rare) """ @@ -4594,7 +4594,7 @@ class ErrorSource(Enum): SYSTEM = "system" -class Status11(Enum): +class Status11(str, Enum): DRAFT = "draft" SCHEDULED = "scheduled" PUBLISHING = "publishing" @@ -4603,13 +4603,13 @@ class Status11(Enum): PARTIAL = "partial" -class Visibility(Enum): +class Visibility(str, Enum): PUBLIC = "public" PRIVATE = "private" UNLISTED = "unlisted" -class GapFreq(Enum): +class GapFreq(str, Enum): """ Interval unit for the gap. Defaults to 'month'. """ @@ -4665,7 +4665,7 @@ class RecyclingConfig(BaseModel): """ -class GapFreq1(Enum): +class GapFreq1(str, Enum): """ Interval unit (week or month) """ @@ -4716,7 +4716,7 @@ class RecyclingState(BaseModel): """ -class ReplySettings(Enum): +class ReplySettings(str, Enum): """ Controls who can reply to the tweet. "following" allows only people you follow, "mentionedUsers" allows only mentioned users, "subscribers" allows only subscribers, "verified" allows only verified users. Omit for default (everyone can reply). For threads, applies to the first tweet only. Cannot be combined with replyToTweetId. """ @@ -4832,7 +4832,7 @@ class ThreadsPlatformData(BaseModel): """ -class ContentType(Enum): +class ContentType(str, Enum): """ Set to 'story' for Page Stories (24h ephemeral) or 'reel' for Reels (short vertical video). Defaults to feed post if omitted. """ @@ -4885,7 +4885,7 @@ class FacebookSettings(BaseModel): """ -class ContentType1(Enum): +class ContentType1(str, Enum): """ Set to 'story' to publish as a Story. Default posts become Reels or feed depending on media. """ @@ -4893,7 +4893,7 @@ class ContentType1(Enum): STORY = "story" -class GraduationStrategy(Enum): +class GraduationStrategy(str, Enum): """ MANUAL (graduate from Instagram app) or SS_PERFORMANCE (auto-graduate if performs well with non-followers) """ @@ -5023,7 +5023,7 @@ class MoneyAmount(BaseModel): """ -class CostType(Enum): +class CostType(str, Enum): """ Campaign cost model (billing event). Defaults to `CPM`. Required when `unitCost` is set so the manual bid applies to an explicit cost model. @@ -5035,7 +5035,7 @@ class CostType(Enum): CPV = "CPV" -class CreativeSelection(Enum): +class CreativeSelection(str, Enum): """ How LinkedIn rotates creatives within the campaign. Defaults to `OPTIMIZED`. """ @@ -5155,7 +5155,7 @@ class Description(BaseModel): """ -class CallToAction(Enum): +class CallToAction(str, Enum): VISIT_ORGANIZATION_COMPANY_PAGE = "VISIT_ORGANIZATION_COMPANY_PAGE" VISIT_ORGANIZATION_LIFE_PAGE = "VISIT_ORGANIZATION_LIFE_PAGE" VISIT_ORGANIZATION_JOBS_PAGE = "VISIT_ORGANIZATION_JOBS_PAGE" @@ -5515,7 +5515,7 @@ class PinterestPlatformData(BaseModel): """ -class Visibility1(Enum): +class Visibility1(str, Enum): """ Video visibility: public (default, anyone can watch), unlisted (link only), private (invite only) """ @@ -5560,7 +5560,7 @@ class YouTubePlatformData(BaseModel): """ -class TopicType(Enum): +class TopicType(str, Enum): """ Post type. STANDARD is a regular update. EVENT requires the event object. OFFER requires the offer object. Defaults to STANDARD if omitted. """ @@ -5570,7 +5570,7 @@ class TopicType(Enum): OFFER = "OFFER" -class Type12(Enum): +class Type12(str, Enum): """ Button action type: LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL """ @@ -5724,7 +5724,7 @@ class GoogleBusinessPlatformData(BaseModel): """ -class CommercialContentType(Enum): +class CommercialContentType(str, Enum): """ Type of commercial content disclosure. Sufficient on its own: "brand_organic" ("Your Brand") implies isBrandOrganicPost and "brand_content" ("Branded Content", @@ -5739,7 +5739,7 @@ class CommercialContentType(Enum): BRAND_CONTENT = "brand_content" -class MediaType(Enum): +class MediaType(str, Enum): """ Optional override. Defaults based on provided media items. """ @@ -5855,7 +5855,7 @@ class TikTokPlatformData(BaseModel): """ -class ParseMode(Enum): +class ParseMode(str, Enum): """ Text formatting mode for the message (default is HTML) """ @@ -5888,7 +5888,7 @@ class TelegramPlatformData(BaseModel): """ -class ContentType2(Enum): +class ContentType2(str, Enum): """ Content type: story (ephemeral 24h, default), saved_story (permanent on Public Profile), spotlight (video feed) """ @@ -6340,7 +6340,7 @@ class Profile(BaseModel): createdAt: AwareDatetime | None = None -class Platform12(Enum): +class Platform12(str, Enum): TIKTOK = "tiktok" INSTAGRAM = "instagram" FACEBOOK = "facebook" @@ -6505,7 +6505,7 @@ class AccountWithFollowerStats(SocialAccount): """ -class Scope(Enum): +class Scope(str, Enum): """ 'full' grants access to all profiles, 'profiles' restricts to specific profiles """ @@ -6520,7 +6520,7 @@ class ProfileId(BaseModel): color: str | None = None -class Permission1(Enum): +class Permission1(str, Enum): """ 'read-write' allows all operations, 'read' restricts to GET requests only """ @@ -6584,7 +6584,7 @@ class ConnectedApp(BaseModel): """ -class BillingSystem(Enum): +class BillingSystem(str, Enum): METRONOME = "metronome" STRIPE = "stripe" @@ -6675,7 +6675,7 @@ class BillingSnapshot(BaseModel): """ -class Granularity(Enum): +class Granularity(str, Enum): DAY = "day" MONTH = "month" TOTAL = "total" @@ -6715,7 +6715,7 @@ class Totals(BaseModel): total: float | None = None -class Product(Enum): +class Product(str, Enum): ACCOUNTS = "accounts" NUMBERS = "numbers" CALLS = "calls" @@ -6761,7 +6761,7 @@ class CallUsage(BaseModel): pstn: Pstn | None = None -class Source2(Enum): +class Source2(str, Enum): """ `cycle` = a real billing period resolved; `window` = trailing/custom window (or cycle fallback). """ @@ -6851,7 +6851,7 @@ class UsageMetering(BaseModel): """ -class BillingSystem1(Enum): +class BillingSystem1(str, Enum): """ Which billing system the account is on. Shape of `usage`/`spend` differs. """ @@ -6860,7 +6860,7 @@ class BillingSystem1(Enum): METRONOME = "metronome" -class BillingPeriod(Enum): +class BillingPeriod(str, Enum): MONTHLY = "monthly" YEARLY = "yearly" @@ -7058,7 +7058,7 @@ class Tier(BaseModel): operationCount: Annotated[int | None, Field(examples=[13])] = None -class Metering(Enum): +class Metering(str, Enum): """ When the method actually bills the user: * `always` — every call is metered @@ -7153,12 +7153,12 @@ class PostAnalytics(BaseModel): lastUpdated: AwareDatetime | None = None -class Status14(Enum): +class Status14(str, Enum): PUBLISHED = "published" FAILED = "failed" -class SyncStatus(Enum): +class SyncStatus(str, Enum): """ Sync state of analytics for this platform """ @@ -7208,7 +7208,7 @@ class AnalyticsOverview(BaseModel): dataStaleness: DataStaleness | None = None -class Status15(Enum): +class Status15(str, Enum): """ Overall post status. "partial" when some platforms published and others failed. """ @@ -7218,7 +7218,7 @@ class Status15(Enum): PARTIAL = "partial" -class SyncStatus1(Enum): +class SyncStatus1(str, Enum): """ Overall sync state across all platforms """ @@ -7236,7 +7236,7 @@ class MediaType1(Enum): TEXT = "text" -class Type13(Enum): +class Type13(str, Enum): IMAGE = "image" VIDEO = "video" @@ -7291,7 +7291,7 @@ class AnalyticsSinglePostResponse(BaseModel): """ -class MediaType2(Enum): +class MediaType2(str, Enum): IMAGE = "image" VIDEO = "video" GIF = "gif" @@ -7354,7 +7354,7 @@ class AnalyticsListResponse(BaseModel): """ -class Aggregation(Enum): +class Aggregation(str, Enum): TOTAL = "TOTAL" @@ -7414,7 +7414,7 @@ class LinkedInAggregateAnalyticsTotalResponse(BaseModel): lastUpdated: AwareDatetime | None = None -class Aggregation1(Enum): +class Aggregation1(str, Enum): DAILY = "DAILY" @@ -7557,7 +7557,7 @@ class FollowerStatsResponse(BaseModel): granularity: str | None = None -class Type15(Enum): +class Type15(str, Enum): IMAGE = "image" VIDEO = "video" DOCUMENT = "document" @@ -7575,7 +7575,7 @@ class MediaUploadResponse(BaseModel): files: List[UploadedFile] | None = None -class Status16(Enum): +class Status16(str, Enum): PENDING = "pending" COMPLETED = "completed" EXPIRED = "expired" @@ -7785,7 +7785,7 @@ class AdDailyMetrics(AdMetrics): """ -class AdStatus(Enum): +class AdStatus(str, Enum): ACTIVE = "active" PAUSED = "paused" PENDING_REVIEW = "pending_review" @@ -7795,7 +7795,7 @@ class AdStatus(Enum): ERROR = "error" -class AdReviewStatus(Enum): +class AdReviewStatus(str, Enum): """ Platform-side review state, independent of the delivery `status` and the `configuredStatus` on/off toggle. `in_review` means the platform is still reviewing. Absent when the platform reports no review signal (e.g. a paused ad whose review state is masked behind the pause). """ @@ -7832,7 +7832,7 @@ class BusinessCenter(BaseModel): """ -class BidStrategy1(Enum): +class BidStrategy1(str, Enum): """ Meta bid strategy. Same enum applies at campaign and ad-set level; ad-set value (when set) overrides campaign-level. Cross-field rules: @@ -7850,7 +7850,7 @@ class BidStrategy1(Enum): LOWEST_COST_WITH_MIN_ROAS = "LOWEST_COST_WITH_MIN_ROAS" -class Type16(Enum): +class Type16(str, Enum): DAILY = "daily" LIFETIME = "lifetime" @@ -7869,7 +7869,7 @@ class Region1(BaseModel): name: str | None = None -class DistanceUnit2(Enum): +class DistanceUnit2(str, Enum): """ Required if radius is set. """ @@ -7901,7 +7901,7 @@ class Metro1(BaseModel): name: str | None = None -class DistanceUnit3(Enum): +class DistanceUnit3(str, Enum): MILE = "mile" KILOMETER = "kilometer" @@ -7921,7 +7921,7 @@ class CustomLocation1(BaseModel): """ -class DistanceUnit4(Enum): +class DistanceUnit4(str, Enum): """ Required if radius is set. """ @@ -7950,7 +7950,7 @@ class Neighborhood(BaseModel): key: str -class DistanceUnit5(Enum): +class DistanceUnit5(str, Enum): MILE = "mile" KILOMETER = "kilometer" @@ -7996,7 +7996,7 @@ class ExcludedLocations(BaseModel): """ -class Gender(Enum): +class Gender(str, Enum): """ Restrict by gender. 'all' (default) targets everyone. Applied on Meta, TikTok and Pinterest. Ignored on Google, LinkedIn and X. """ @@ -8006,7 +8006,7 @@ class Gender(Enum): FEMALE = "female" -class IncomeTier(Enum): +class IncomeTier(str, Enum): """ Normalized household-income tier (ZIP/percentile based). Meta and TikTok express all four. Google maps only `top_10` (its INCOME_RANGE_90_UP); other @@ -8126,7 +8126,7 @@ class TargetingSpec(BaseModel): """ -class Platform13(Enum): +class Platform13(str, Enum): FACEBOOK = "facebook" INSTAGRAM = "instagram" TIKTOK = "tiktok" @@ -8153,12 +8153,12 @@ class ReviewStatus(RootModel[AdReviewStatus]): root: AdReviewStatus -class AdType(Enum): +class AdType(str, Enum): BOOST = "boost" STANDALONE = "standalone" -class Goal(Enum): +class Goal(str, Enum): """ Available goals vary by platform. Meta (Facebook/Instagram) supports all 9 (incl. `lead_conversion` = website pixel lead optimization and `catalog_sales` = Advantage+ catalog ads). TikTok supports the 7 non-`lead_conversion` goals. LinkedIn supports all except app_promotion / lead_conversion. Twitter/X supports engagement, traffic, awareness, video_views, app_promotion. Pinterest and Google Ads support only engagement, traffic, awareness, video_views. """ @@ -8866,7 +8866,7 @@ class Item(BaseModel): category: str | None = None -class ActionSource(Enum): +class ActionSource(str, Enum): """ Where the conversion happened. Used by Meta. Google also requires an event source internally; omitting this field sends OTHER to Google. Send an explicit value for accurate origin reporting. """ @@ -8946,7 +8946,7 @@ class ConversionEvent(BaseModel): """ -class Status22(Enum): +class Status22(str, Enum): """ For LinkedIn, `inactive` means the rule is soft-deleted (`enabled: false`). @@ -8993,7 +8993,7 @@ class ConversionDestination(BaseModel): """ -class AdType1(Enum): +class AdType1(str, Enum): SINGLE = "single" @@ -9013,7 +9013,7 @@ class CtwaSingleResponse(BaseModel): message: str -class AdType2(Enum): +class AdType2(str, Enum): MULTI = "multi" @@ -9038,11 +9038,11 @@ class CtwaMultiResponse(BaseModel): message: str -class Platform16(Enum): +class Platform16(str, Enum): METAADS = "metaads" -class Kind(Enum): +class Kind(str, Enum): """ Platform-native flavor of the tag (Meta: `pixel`). """ @@ -9052,7 +9052,7 @@ class Kind(Enum): INSIGHT_TAG = "insight_tag" -class Status23(Enum): +class Status23(str, Enum): """ `inactive` when the platform reports the tag as broken/unavailable. """ diff --git a/src/late/resources/_generated/account_groups.py b/src/late/resources/_generated/account_groups.py index 0931a1d..139fe09 100644 --- a/src/late/resources/_generated/account_groups.py +++ b/src/late/resources/_generated/account_groups.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/account_settings.py b/src/late/resources/_generated/account_settings.py index 1489a6f..fd63f01 100644 --- a/src/late/resources/_generated/account_settings.py +++ b/src/late/resources/_generated/account_settings.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/accounts.py b/src/late/resources/_generated/accounts.py index c4874e0..547572a 100644 --- a/src/late/resources/_generated/accounts.py +++ b/src/late/resources/_generated/accounts.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_accounts.py b/src/late/resources/_generated/ad_accounts.py index 1385024..832e9ea 100644 --- a/src/late/resources/_generated/ad_accounts.py +++ b/src/late/resources/_generated/ad_accounts.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_audiences.py b/src/late/resources/_generated/ad_audiences.py index 104f832..0dff37a 100644 --- a/src/late/resources/_generated/ad_audiences.py +++ b/src/late/resources/_generated/ad_audiences.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_campaigns.py b/src/late/resources/_generated/ad_campaigns.py index 9ca0974..45766c8 100644 --- a/src/late/resources/_generated/ad_campaigns.py +++ b/src/late/resources/_generated/ad_campaigns.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_creatives.py b/src/late/resources/_generated/ad_creatives.py index 1327b51..561432e 100644 --- a/src/late/resources/_generated/ad_creatives.py +++ b/src/late/resources/_generated/ad_creatives.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_insights.py b/src/late/resources/_generated/ad_insights.py index c154108..470cf7b 100644 --- a/src/late/resources/_generated/ad_insights.py +++ b/src/late/resources/_generated/ad_insights.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/ad_targeting.py b/src/late/resources/_generated/ad_targeting.py index de709d3..662844b 100644 --- a/src/late/resources/_generated/ad_targeting.py +++ b/src/late/resources/_generated/ad_targeting.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/analytics.py b/src/late/resources/_generated/analytics.py index 5e61d97..6e6d100 100644 --- a/src/late/resources/_generated/analytics.py +++ b/src/late/resources/_generated/analytics.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/api_keys.py b/src/late/resources/_generated/api_keys.py index 285cbe8..9adbb95 100644 --- a/src/late/resources/_generated/api_keys.py +++ b/src/late/resources/_generated/api_keys.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/broadcasts.py b/src/late/resources/_generated/broadcasts.py index f434b28..fbac450 100644 --- a/src/late/resources/_generated/broadcasts.py +++ b/src/late/resources/_generated/broadcasts.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/calls.py b/src/late/resources/_generated/calls.py index d39e4cd..ccf932a 100644 --- a/src/late/resources/_generated/calls.py +++ b/src/late/resources/_generated/calls.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/comment_automations.py b/src/late/resources/_generated/comment_automations.py index 61a71b7..ea150d5 100644 --- a/src/late/resources/_generated/comment_automations.py +++ b/src/late/resources/_generated/comment_automations.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/comments.py b/src/late/resources/_generated/comments.py index 75b8605..2b2e371 100644 --- a/src/late/resources/_generated/comments.py +++ b/src/late/resources/_generated/comments.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/connect.py b/src/late/resources/_generated/connect.py index f07afde..61a13e8 100644 --- a/src/late/resources/_generated/connect.py +++ b/src/late/resources/_generated/connect.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/connected_apps.py b/src/late/resources/_generated/connected_apps.py index c734173..fb21e73 100644 --- a/src/late/resources/_generated/connected_apps.py +++ b/src/late/resources/_generated/connected_apps.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/contacts.py b/src/late/resources/_generated/contacts.py index ed7daa2..12f5129 100644 --- a/src/late/resources/_generated/contacts.py +++ b/src/late/resources/_generated/contacts.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/conversions.py b/src/late/resources/_generated/conversions.py index 32e25db..d09bcc9 100644 --- a/src/late/resources/_generated/conversions.py +++ b/src/late/resources/_generated/conversions.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/custom_fields.py b/src/late/resources/_generated/custom_fields.py index 7a2b244..289452a 100644 --- a/src/late/resources/_generated/custom_fields.py +++ b/src/late/resources/_generated/custom_fields.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/discord.py b/src/late/resources/_generated/discord.py index cf12005..efd2fa9 100644 --- a/src/late/resources/_generated/discord.py +++ b/src/late/resources/_generated/discord.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/gmb_services.py b/src/late/resources/_generated/gmb_services.py index c008273..5a9341c 100644 --- a/src/late/resources/_generated/gmb_services.py +++ b/src/late/resources/_generated/gmb_services.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/gmb_verifications.py b/src/late/resources/_generated/gmb_verifications.py index ce67b43..486012f 100644 --- a/src/late/resources/_generated/gmb_verifications.py +++ b/src/late/resources/_generated/gmb_verifications.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/inbox_analytics.py b/src/late/resources/_generated/inbox_analytics.py index cae90d0..d9978e1 100644 --- a/src/late/resources/_generated/inbox_analytics.py +++ b/src/late/resources/_generated/inbox_analytics.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/instagram.py b/src/late/resources/_generated/instagram.py index 68a5fe5..d9f4463 100644 --- a/src/late/resources/_generated/instagram.py +++ b/src/late/resources/_generated/instagram.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/invites.py b/src/late/resources/_generated/invites.py index c69909c..59fc344 100644 --- a/src/late/resources/_generated/invites.py +++ b/src/late/resources/_generated/invites.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/lead_gen.py b/src/late/resources/_generated/lead_gen.py index 63eab53..4114502 100644 --- a/src/late/resources/_generated/lead_gen.py +++ b/src/late/resources/_generated/lead_gen.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/logs.py b/src/late/resources/_generated/logs.py index 88b0e7f..73c19ad 100644 --- a/src/late/resources/_generated/logs.py +++ b/src/late/resources/_generated/logs.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/media.py b/src/late/resources/_generated/media.py index bd6aeba..2f544ab 100644 --- a/src/late/resources/_generated/media.py +++ b/src/late/resources/_generated/media.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/mentions.py b/src/late/resources/_generated/mentions.py index 1614e39..68b6ab2 100644 --- a/src/late/resources/_generated/mentions.py +++ b/src/late/resources/_generated/mentions.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/messages.py b/src/late/resources/_generated/messages.py index ff84bbb..2255f38 100644 --- a/src/late/resources/_generated/messages.py +++ b/src/late/resources/_generated/messages.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/messaging_ads.py b/src/late/resources/_generated/messaging_ads.py index 15f44ed..c0d76d4 100644 --- a/src/late/resources/_generated/messaging_ads.py +++ b/src/late/resources/_generated/messaging_ads.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/phone_numbers.py b/src/late/resources/_generated/phone_numbers.py index 6c28a09..06d07c1 100644 --- a/src/late/resources/_generated/phone_numbers.py +++ b/src/late/resources/_generated/phone_numbers.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/posts.py b/src/late/resources/_generated/posts.py index c2eaf1b..c1a2a77 100644 --- a/src/late/resources/_generated/posts.py +++ b/src/late/resources/_generated/posts.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/profiles.py b/src/late/resources/_generated/profiles.py index c34569c..d9de304 100644 --- a/src/late/resources/_generated/profiles.py +++ b/src/late/resources/_generated/profiles.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/queue.py b/src/late/resources/_generated/queue.py index a59eaa1..eaedf48 100644 --- a/src/late/resources/_generated/queue.py +++ b/src/late/resources/_generated/queue.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/reach_and_frequency.py b/src/late/resources/_generated/reach_and_frequency.py index 688215f..0ea7e94 100644 --- a/src/late/resources/_generated/reach_and_frequency.py +++ b/src/late/resources/_generated/reach_and_frequency.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/reddit.py b/src/late/resources/_generated/reddit.py index 44c5941..a991285 100644 --- a/src/late/resources/_generated/reddit.py +++ b/src/late/resources/_generated/reddit.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/reviews.py b/src/late/resources/_generated/reviews.py index 4246fe7..767231f 100644 --- a/src/late/resources/_generated/reviews.py +++ b/src/late/resources/_generated/reviews.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/sequences.py b/src/late/resources/_generated/sequences.py index 42f499c..445d962 100644 --- a/src/late/resources/_generated/sequences.py +++ b/src/late/resources/_generated/sequences.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/sms.py b/src/late/resources/_generated/sms.py index 71ca6c1..1f54bdf 100644 --- a/src/late/resources/_generated/sms.py +++ b/src/late/resources/_generated/sms.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/tracking_tags.py b/src/late/resources/_generated/tracking_tags.py index c202feb..be28471 100644 --- a/src/late/resources/_generated/tracking_tags.py +++ b/src/late/resources/_generated/tracking_tags.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/twitter_engagement.py b/src/late/resources/_generated/twitter_engagement.py index d98ed21..246308a 100644 --- a/src/late/resources/_generated/twitter_engagement.py +++ b/src/late/resources/_generated/twitter_engagement.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/usage.py b/src/late/resources/_generated/usage.py index 7df4e4b..5131264 100644 --- a/src/late/resources/_generated/usage.py +++ b/src/late/resources/_generated/usage.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/users.py b/src/late/resources/_generated/users.py index c147c05..1dd2e93 100644 --- a/src/late/resources/_generated/users.py +++ b/src/late/resources/_generated/users.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/validate.py b/src/late/resources/_generated/validate.py index 64d8bc7..d937f83 100644 --- a/src/late/resources/_generated/validate.py +++ b/src/late/resources/_generated/validate.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/verify.py b/src/late/resources/_generated/verify.py index 7f6c568..9364825 100644 --- a/src/late/resources/_generated/verify.py +++ b/src/late/resources/_generated/verify.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/voice.py b/src/late/resources/_generated/voice.py index 63370b6..a2bdfcf 100644 --- a/src/late/resources/_generated/voice.py +++ b/src/late/resources/_generated/voice.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/webhooks.py b/src/late/resources/_generated/webhooks.py index 76b70bf..08b798e 100644 --- a/src/late/resources/_generated/webhooks.py +++ b/src/late/resources/_generated/webhooks.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp.py b/src/late/resources/_generated/whatsapp.py index 2edb9fa..a529620 100644 --- a/src/late/resources/_generated/whatsapp.py +++ b/src/late/resources/_generated/whatsapp.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp_calling.py b/src/late/resources/_generated/whatsapp_calling.py index 1732bb8..d4eff07 100644 --- a/src/late/resources/_generated/whatsapp_calling.py +++ b/src/late/resources/_generated/whatsapp_calling.py @@ -30,17 +30,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -48,6 +65,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp_flows.py b/src/late/resources/_generated/whatsapp_flows.py index 62c30c4..657c54d 100644 --- a/src/late/resources/_generated/whatsapp_flows.py +++ b/src/late/resources/_generated/whatsapp_flows.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp_phone_numbers.py b/src/late/resources/_generated/whatsapp_phone_numbers.py index 2d0f41c..8241e35 100644 --- a/src/late/resources/_generated/whatsapp_phone_numbers.py +++ b/src/late/resources/_generated/whatsapp_phone_numbers.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp_sandbox.py b/src/late/resources/_generated/whatsapp_sandbox.py index 328d757..60449d2 100644 --- a/src/late/resources/_generated/whatsapp_sandbox.py +++ b/src/late/resources/_generated/whatsapp_sandbox.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/whatsapp_templates.py b/src/late/resources/_generated/whatsapp_templates.py index a9a9ae3..cc945d6 100644 --- a/src/late/resources/_generated/whatsapp_templates.py +++ b/src/late/resources/_generated/whatsapp_templates.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/_generated/workflows.py b/src/late/resources/_generated/workflows.py index 50ba61f..a19246f 100644 --- a/src/late/resources/_generated/workflows.py +++ b/src/late/resources/_generated/workflows.py @@ -28,17 +28,34 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: default for optional string args, and the API rejects empty query values (e.g. ``platform=``) with a 400. Filtering here keeps both direct SDK callers and MCP tool callers safe. + + Enum members are unwrapped to their value: httpx serializes params + via str(), which yields "ClassName.MEMBER" for Enum members, so + e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as + ``status=PostStatus.FAILED`` instead of ``status=failed``. """ + from enum import Enum 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 and v != ""} + result: dict[str, Any] = {} + for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value + if v is None or v == "": + continue + result[to_camel(k)] = v + return result def _build_payload(self, **kwargs: Any) -> dict[str, Any]: - """Build request payload, filtering None values.""" + """Build request payload, filtering None values. Enum members are + unwrapped to their value so JSON bodies carry e.g. "failed" rather + than a raw Enum member (plain Enum members are not JSON-serializable). + """ from datetime import datetime + from enum import Enum def to_camel(s: str) -> str: parts = s.split("_") @@ -46,6 +63,8 @@ def to_camel(s: str) -> str: result: dict[str, Any] = {} for k, v in kwargs.items(): + if isinstance(v, Enum): + v = v.value if v is None: continue if isinstance(v, datetime): diff --git a/src/late/resources/base.py b/src/late/resources/base.py index 436bc7b..b241c4b 100644 --- a/src/late/resources/base.py +++ b/src/late/resources/base.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime +from enum import Enum from typing import TYPE_CHECKING, Any, Generic, TypeVar from pydantic import BaseModel @@ -21,6 +22,20 @@ def _to_camel_case(snake_str: str) -> str: return components[0] + "".join(x.title() for x in components[1:]) +def _normalize_value(value: Any) -> Any: + """ + Normalize a value for the wire. + + Enum members are unwrapped to their value: httpx serializes params via + str(), and str() of an Enum member yields "ClassName.MEMBER" rather than + the member's value — so `status=PostStatus.FAILED` would otherwise reach + the API as `status=PostStatus.FAILED` instead of `status=failed`. + """ + if isinstance(value, Enum): + return value.value + return value + + class BaseResource(Generic[T]): """ Base class for API resources. @@ -51,7 +66,11 @@ def _build_params(self, **kwargs: Any) -> dict[str, Any]: Returns: Dictionary with non-None values and camelCase keys """ - return {_to_camel_case(k): v for k, v in kwargs.items() if v is not None} + return { + _to_camel_case(k): _normalize_value(v) + for k, v in kwargs.items() + if v is not None + } def _build_payload(self, **kwargs: Any) -> dict[str, Any]: """ @@ -72,7 +91,7 @@ def _build_payload(self, **kwargs: Any) -> dict[str, Any]: if isinstance(value, datetime): payload[camel_key] = value.isoformat() else: - payload[camel_key] = value + payload[camel_key] = _normalize_value(value) return payload def _path(self, *parts: str) -> str: diff --git a/tests/test_integration.py b/tests/test_integration.py index f785d07..255831a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1056,6 +1056,56 @@ def test_build_params_converts_to_camel_case(self, client: Late) -> None: assert "includeHidden" in params assert "profile_id" not in params + def test_build_params_unwraps_enums(self, client: Late) -> None: + """Enum params must serialize as their value, not "ClassName.MEMBER". + + Regression test for zernio-claude-plugin#1: httpx serializes params + via str(), so passing PostStatus.FAILED through unchanged produced + `status=PostStatus.FAILED` on the wire and the API matched nothing. + """ + import httpx + + from late import Platform, PostStatus + + params = client.posts._build_params( + status=PostStatus.FAILED, + platform=Platform.TWITTER, + limit=50, + ) + + assert params["status"] == "failed" + assert params["platform"] == "twitter" + assert str(httpx.QueryParams(params)) == "status=failed&platform=twitter&limit=50" + + def test_build_payload_unwraps_enums(self, client: Late) -> None: + """Enum payload values must serialize as their value.""" + from late import PostStatus + + payload = client.posts._build_payload( + content="Test", + status=PostStatus.DRAFT, + ) + + assert payload["status"] == "draft" + + def test_generated_status_enum_matches_public_post_status(self) -> None: + """Generated post-status enum must stay value-compatible with PostStatus. + + The generated enum is renamed on regeneration (Status7, Status10, ...); + what matters is that its members are str-mixins equal to the public + PostStatus values, so status comparisons in the MCP tools keep working. + """ + from late import PostStatus + from late.models._generated.models import PostGetResponse + + post = PostGetResponse.model_validate( + {"post": {"_id": "abc", "status": "failed"}} + ).post + assert post is not None + assert post.status == "failed" + assert post.status == PostStatus.FAILED + assert post.status.value == PostStatus.FAILED.value + def test_build_payload_handles_datetime(self, client: Late) -> None: """Test that _build_payload serializes datetime objects.""" dt = datetime(2024, 12, 25, 10, 0, 0) diff --git a/tests/test_mcp_retry_regression.py b/tests/test_mcp_retry_regression.py new file mode 100644 index 0000000..f30c57d --- /dev/null +++ b/tests/test_mcp_retry_regression.py @@ -0,0 +1,153 @@ +""" +Regression tests for zernio-claude-plugin#1: posts_retry / posts_retry_all_failed +never recognized failed posts. + +Two bugs conspired: + +1. ``posts_retry`` compared the post's status (a generated enum, e.g. + ``Status10.FAILED``) against the handwritten ``PostStatus.FAILED`` — two + different enum classes never compare equal, so every failed post was + rejected with "is not in failed status". +2. ``posts_retry_all_failed`` passed ``PostStatus.FAILED`` into the query + string, where httpx serialized it via str() as ``status=PostStatus.FAILED`` + instead of ``status=failed`` — so the API matched nothing and the tool + reported "No failed posts to retry". + +These tests drive the real MCP tool functions against a fake in-memory Zernio +API (httpx.MockTransport — no network). The fake API is strict: it only +treats ``status=failed`` (the literal value) as a match, exactly like the real +API, so bug 2 cannot regress silently. It also records every request so we can +assert the exact wire format. +""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest + +import late.client.base as client_base +from late import Late +from late.mcp import server as mcp_server + +FAILED_POST = { + "_id": "p1", + "content": "Hello world", + "status": "failed", + "platforms": [{"platform": "twitter", "accountId": "a1"}], +} + + +class FakeZernioAPI: + """Minimal in-memory Zernio API with one failed post.""" + + def __init__(self) -> None: + self.requests: list[httpx.Request] = [] + self.retried: list[str] = [] + + def handler(self, request: httpx.Request) -> httpx.Response: + self.requests.append(request) + path = request.url.path + + if request.method == "GET" and path == "/api/v1/posts": + # Strict, like the real API: only the literal value "failed" + # matches. "PostStatus.FAILED" (the bug) matches nothing. + if request.url.params.get("status") == "failed": + return self._json({"posts": [FAILED_POST], "pagination": {}}) + return self._json({"posts": [], "pagination": {}}) + + if request.method == "GET" and path == "/api/v1/posts/p1": + return self._json({"post": FAILED_POST}) + + if request.method == "POST" and path == "/api/v1/posts/p1/retry": + self.retried.append("p1") + return self._json({"message": "Post queued for retry"}) + + return httpx.Response(404, json={"error": f"unexpected: {path}"}) + + @staticmethod + def _json(body: dict[str, Any]) -> httpx.Response: + return httpx.Response( + 200, + content=json.dumps(body), + headers={"Content-Type": "application/json"}, + ) + + +@pytest.fixture() +def fake_api(monkeypatch: pytest.MonkeyPatch) -> FakeZernioAPI: + """Route all SDK HTTP traffic to the fake API and wire up the MCP client.""" + api = FakeZernioAPI() + real_client = httpx.Client + + def patched_client(**kwargs: Any) -> httpx.Client: + kwargs["transport"] = httpx.MockTransport(api.handler) + return real_client(**kwargs) + + monkeypatch.setattr(client_base.httpx, "Client", patched_client) + monkeypatch.setattr(mcp_server, "_get_client", lambda: Late(api_key="test-key")) + return api + + +class TestIssue1PostsRetry: + def test_posts_retry_accepts_failed_post(self, fake_api: FakeZernioAPI) -> None: + """Bug 1: the status guard rejected every failed post.""" + result = mcp_server.posts_retry("p1") + + assert "queued for retry" in result + assert "is not in failed status" not in result + assert fake_api.retried == ["p1"] + + def test_posts_retry_all_failed_finds_the_failed_post( + self, fake_api: FakeZernioAPI + ) -> None: + """Bug 2: the list query serialized the enum wrong and found nothing.""" + result = mcp_server.posts_retry_all_failed() + + assert "Retried 1 post(s)" in result + assert "No failed posts to retry" not in result + assert fake_api.retried == ["p1"] + + def test_posts_list_failed_finds_the_failed_post( + self, fake_api: FakeZernioAPI + ) -> None: + """posts_list_failed had bug 2 as well.""" + result = mcp_server.posts_list_failed() + + assert "Found 1 failed post(s)" in result + assert any(r.url.path == "/api/v1/posts" for r in fake_api.requests) + + def test_status_filter_reaches_api_as_plain_value( + self, fake_api: FakeZernioAPI + ) -> None: + """The wire format itself: status=failed, never status=PostStatus.FAILED.""" + mcp_server.posts_retry_all_failed() + + list_requests = [r for r in fake_api.requests if r.url.path == "/api/v1/posts"] + assert list_requests, "expected a posts list request" + for request in list_requests: + assert request.url.params.get("status") == "failed" + assert "PostStatus" not in str(request.url) + + def test_posts_retry_reports_clean_status_for_non_failed_post( + self, fake_api: FakeZernioAPI, monkeypatch: pytest.MonkeyPatch + ) -> None: + """The guard message must show "scheduled", not "Status10.SCHEDULED".""" + scheduled = dict(FAILED_POST, status="scheduled") + + original = fake_api.handler + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "GET" and request.url.path == "/api/v1/posts/p1": + return FakeZernioAPI._json({"post": scheduled}) + return original(request) + + monkeypatch.setattr(fake_api, "handler", handler) + + result = mcp_server.posts_retry("p1") + + assert "is not in failed status" in result + assert "(current: scheduled)" in result + assert "Status" not in result.replace("failed status", "")