diff --git a/CHANGELOG.md b/CHANGELOG.md index f38473e..559401e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.1 (2026-10-01) + + +### Bug Fixes + +* **python:** type client methods as their decoded results +* **python:** type client methods as their decoded results + ## 0.2.0 (2026-10-01) diff --git a/Cargo.lock b/Cargo.lock index 3409dca..3db3cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,7 +993,7 @@ dependencies = [ [[package]] name = "photonhq-api" -version = "0.2.0" +version = "0.2.1" dependencies = [ "bytes", "httpdate", diff --git a/package-lock.json b/package-lock.json index 60488e6..7184002 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1019,7 +1019,7 @@ }, "packages/typescript": { "name": "@photon-ai/api", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "dependencies": { "zod": "^4.5.0" diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml index 86c4a7b..67d9a77 100644 --- a/packages/python/pyproject.toml +++ b/packages/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "photonhq-api" -version = "0.2.0" +version = "0.2.1" description = "Generated Photon API RPC client" readme = "README.md" requires-python = ">=3.11" diff --git a/packages/python/src/photon_api/client.py b/packages/python/src/photon_api/client.py index bece227..59f32c1 100644 --- a/packages/python/src/photon_api/client.py +++ b/packages/python/src/photon_api/client.py @@ -5,7 +5,7 @@ import httpx from .config_generated import DEFAULT_BASE_URL -from .rpc_generated import AsyncRoot, SyncRoot +from .rpc_generated import AsyncRawRoot, AsyncRoot, SyncRawRoot, SyncRoot from .transport import ( DEFAULT_MAX_RETRY_AFTER, AsyncHeaderProvider, @@ -35,7 +35,7 @@ def __init__( max_retry_after=max_retry_after, ) super().__init__(self._transport) - self.raw = SyncRoot(self._transport, raw=True) + self.raw = SyncRawRoot(self._transport) def close(self) -> None: self._transport.close() @@ -67,7 +67,7 @@ def __init__( max_retry_after=max_retry_after, ) super().__init__(self._transport) - self.raw = AsyncRoot(self._transport, raw=True) + self.raw = AsyncRawRoot(self._transport) async def close(self) -> None: await self._transport.close() diff --git a/packages/python/src/photon_api/rpc_generated.py b/packages/python/src/photon_api/rpc_generated.py index 8315060..e75aef9 100644 --- a/packages/python/src/photon_api/rpc_generated.py +++ b/packages/python/src/photon_api/rpc_generated.py @@ -4016,2521 +4016,3461 @@ class UploadAttachmentInput(BaseModel): ) -class SyncProjectsPlatformsImessageAssignmentsResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsPlatformsImessageAssignmentsResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def create( self, input: CreateSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> RawResponse[models.SharedLineAssignment]: "Create shared line assignment\n\nMaps an end user's iMessage handle — an E.164 phone number or an email address — onto one of the project's pooled shared iMessage lines, consuming a seat from the project's entitlement. The assigned number is allocated by the server. When an email address is supplied in `email` the user is sent an invite asynchronously to that address; it is never inferred from the handle, and the response never reports whether the send succeeded. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_SHARED_LINE_ASSIGNMENT, payload) - def get( - self, input: GetSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + def get(self, input: GetSharedLineAssignmentInput) -> RawResponse[models.SharedLineAssignment]: "Get shared line assignment\n\nReads one shared line assignment. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_SHARED_LINE_ASSIGNMENT, payload) def list( self, input: ListSharedLineAssignmentsInput - ) -> models.SharedLineAssignmentPage | RawResponse[models.SharedLineAssignmentPage]: + ) -> RawResponse[models.SharedLineAssignmentPage]: "List shared line assignments\n\nLists the project's shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_SHARED_LINE_ASSIGNMENTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_SHARED_LINE_ASSIGNMENTS, payload) def release( self, input: ReleaseSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> RawResponse[models.SharedLineAssignment]: "Release shared line assignment\n\nReleases a shared line assignment, freeing both its seat and its handle for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RELEASE_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RELEASE_SHARED_LINE_ASSIGNMENT, payload) -class SyncProjectsPlatformsImessageResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsPlatformsImessageResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw - self.assignments = SyncProjectsPlatformsImessageAssignmentsResource(transport, raw) + self.assignments = SyncRawProjectsPlatformsImessageAssignmentsResource(transport) -class SyncProjectsPlatformsResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsPlatformsResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw - self.imessage = SyncProjectsPlatformsImessageResource(transport, raw) + self.imessage = SyncRawProjectsPlatformsImessageResource(transport) def assign_sms_line_campaign( self, input: AssignSmsLineCampaignInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Assign or replace SMS line campaign\n\nAttach a ready campaign from this project’s organization to its line. Requires platforms:write for the project; human and machine actors retain their authenticated identity. Requires a permanent Idempotency-Key and the current assignment version. Provider provisioning runs asynchronously." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_ASSIGN_SMS_LINE_CAMPAIGN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_ASSIGN_SMS_LINE_CAMPAIGN, payload) def assign_voice_line_profile( self, input: AssignVoiceLineProfileInput - ) -> models.VoiceLineProfileAssignment | RawResponse[models.VoiceLineProfileAssignment]: + ) -> RawResponse[models.VoiceLineProfileAssignment]: "Assign Voice line profile\n\nAssigns or replaces a line's explicit additional-profile override when the resource version matches. The current default cannot be assigned explicitly. The pstn_voice ability remains the admission source of truth. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_ASSIGN_VOICE_LINE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_ASSIGN_VOICE_LINE_PROFILE, payload) def batch_update_voice_line_profile_assignments( self, input: BatchUpdateVoiceLineProfileAssignmentsInput - ) -> ( - models.BatchUpdateVoiceLineProfileAssignmentsResponse - | RawResponse[models.BatchUpdateVoiceLineProfileAssignmentsResponse] - ): + ) -> RawResponse[models.BatchUpdateVoiceLineProfileAssignmentsResponse]: "Batch update Voice line profile assignments\n\nAtomically sets additional-profile overrides or switches lines back to the project default for up to 100 Voice-capable lines. A null profileId means use the default. Every expected resource version must match or no line changes. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_BATCH_UPDATE_VOICE_LINE_PROFILE_ASSIGNMENTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_BATCH_UPDATE_VOICE_LINE_PROFILE_ASSIGNMENTS, payload) - def cancel_operation( - self, input: CancelOperationInput - ) -> models.Operation | RawResponse[models.Operation]: + def cancel_operation(self, input: CancelOperationInput) -> RawResponse[models.Operation]: "Cancel operation\n\nWithdraws a provision that has not been fulfilled yet. This is an operations action rather than a DELETE, because there is nothing to delete: no resource exists until the work commits. Whether it is accepted depends on the resource type — a dedicated iMessage line may sit waiting on inventory for hours and withdrawing costs nothing, while an SMS number is cancellable during inventory waiting and answers 409 once the workflow commits to its first provider order. Campaign assignment and detachment operations cannot be cancelled in any state. Wait for completion before requesting another change; that new change is not a guaranteed rollback. The output-only `cancellable` field is a snapshot; the cancellation transaction always checks the current phase under a row lock. A cancel that loses the race against the work finishing also answers 409: the resource exists and is billed for, so what you want then is to release it. Nothing is charged for a cancelled provision — billing runs after the work, so there is never anything to refund. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CANCEL_OPERATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CANCEL_OPERATION, payload) def configure_voice_profile_outbound( self, input: ConfigureVoiceProfileOutboundInput - ) -> ( - models.ConfigureVoiceProfileOutboundResponse - | RawResponse[models.ConfigureVoiceProfileOutboundResponse] - ): + ) -> RawResponse[models.ConfigureVoiceProfileOutboundResponse]: "Configure Voice profile outbound credential\n\nConfigures a SIP credential for outbound calls from a profile when the shared profile version matches. authentication.algorithm is required: SHA-256 is recommended, while MD5 is a weaker legacy option supported over UDP, TCP, and TLS; TLS is strongly recommended because UDP and TCP do not encrypt SIP signaling. The profileId may identify the default or an additional profile. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CONFIGURE_VOICE_PROFILE_OUTBOUND, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CONFIGURE_VOICE_PROFILE_OUTBOUND, payload) - def connect_email_domain( - self, input: ConnectEmailDomainInput - ) -> models.Operation | RawResponse[models.Operation]: + def connect_email_domain(self, input: ConnectEmailDomainInput) -> RawResponse[models.Operation]: "Connect email domain\n\nReserves a normalized DNS domain and starts its durable email-provider setup. The accepted provision consumes one email-domain entitlement slot until it fails, is cancelled, or becomes a live resource; the plan's email.max_email_domains value sets the project limit. The customer resource does not exist until provider identity and DNS setup reach READY; poll the returned operation for progress. A domain may have only one unfinished provision or live resource globally. Email domains have no additional per-domain charge. The Idempotency-Key is required and permanent: replaying the same key and canonical domain returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CONNECT_EMAIL_DOMAIN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CONNECT_EMAIL_DOMAIN, payload) - def connect_telegram_bot( - self, input: ConnectTelegramBotInput - ) -> models.Operation | RawResponse[models.Operation]: + def connect_telegram_bot(self, input: ConnectTelegramBotInput) -> RawResponse[models.Operation]: "Connect Telegram bot\n\nStarts a free managed Telegram bot connection. Each project may have one unfinished Telegram provision, including user interaction and failure cleanup. A different Idempotency-Key while one is active returns 409 TELEGRAM_PROVISION_IN_PROGRESS with its operationId and operationUrl; resume it, cancel it while cancellation is available, or wait for it to finish. Rejected keys remain reusable. Open detail.setupUrl to connect an existing managed bot or create a new one with the project's default agent name or a custom display name, then poll Location. The link remains usable while the operation is active and never expires. Replaying the same Idempotency-Key returns the original operation, even after completion or while a newer setup is active. POST, GET and list share the same operation details. The API includes detail.setupUrl only for callers with platforms:write for the project; read-only callers receive the other details unchanged." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CONNECT_TELEGRAM_BOT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CONNECT_TELEGRAM_BOT, payload) def connect_whatsapp_business( self, input: ConnectWhatsappBusinessInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Connect WhatsApp Business\n\nExchanges the authorization code Embedded Signup returned and connects exactly the selected phone number as one `whatsapp_sender`. Send the WABA id and phone-number id emitted by the same popup attempt; both are treated as selectors and verified against Meta before use. A selected number that matches a non-retired, same-project `voip_line` is linked to it; a number absent from Photon inventory stays unbound; a matching non-retired `cosmos_line`, foreign VoIP line or unassigned VoIP line fails the operation before registration. Connecting is free — no plan requirement — but Billing must report the project's organization as ready with a payment method on file. The Idempotency-Key is required and permanent: replaying the same key returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CONNECT_WHATSAPP_BUSINESS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CONNECT_WHATSAPP_BUSINESS, payload) def create_default_voice_profile( self, input: CreateDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> RawResponse[models.VoiceProfile]: "Create default Voice profile\n\nCreates the project default Voice profile when absent. An identical replay returns the existing default without changing its version; a different existing default conflicts. Direction configuration is managed separately. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_DEFAULT_VOICE_PROFILE, payload) def create_voice_profile( self, input: CreateVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> RawResponse[models.VoiceProfile]: "Create Voice profile\n\nCreates a direction-neutral additional Voice profile. The project default must already exist. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_VOICE_PROFILE, payload) def create_whatsapp_shared_line_assignment( self, input: CreateWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> RawResponse[models.SharedLineAssignment]: "Create WhatsApp shared line assignment\n\nMaps an end user's phone number onto one of the project's pooled shared WhatsApp lines, consuming a seat from the project's WhatsApp entitlement. The assigned number is allocated by the server. When an email address is supplied the user is sent an invite asynchronously; the response never reports whether that succeeded. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) def create_whatsapp_voip_sender( self, input: CreateWhatsappVoipSenderInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Create a VoIP-backed WhatsApp sender\n\nRegisters an active, SMS-capable Photon VoIP line on this project's connected WhatsApp Business Account. The account is resolved server-side; callers never select a WABA. The platform creates or reuses the Meta number, requests and consumes the SMS ownership code internally, verifies it, and registers the sender. displayName is optional; when omitted the project agent profile name is snapshotted before acceptance. The VoIP line remains a separate resource and never receives the whatsapp_business ability." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_WHATSAPP_VOIP_SENDER, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_WHATSAPP_VOIP_SENDER, payload) - def delete_voice_profile(self, input: DeleteVoiceProfileInput) -> None | RawResponse[None]: + def delete_voice_profile(self, input: DeleteVoiceProfileInput) -> RawResponse[None]: "Delete Voice profile\n\nDeletes an additional profile when expectedVersion matches. Assigned profiles require force=true, which atomically removes every stored override so affected lines follow the default. The default can never be deleted. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DELETE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_VOICE_PROFILE, payload) def delete_voice_profile_inbound( self, input: DeleteVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: "Remove Voice profile inbound configuration\n\nRemoves a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. The profile and its line assignments remain. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DELETE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_VOICE_PROFILE_INBOUND, payload) def delete_voice_profile_outbound( self, input: DeleteVoiceProfileOutboundInput - ) -> ( - models.DeleteVoiceProfileOutboundResponse - | RawResponse[models.DeleteVoiceProfileOutboundResponse] - ): + ) -> RawResponse[models.DeleteVoiceProfileOutboundResponse]: "Revoke Voice profile outbound credential\n\nRevokes outbound calling for a profile when the shared profile version matches. The profileId may identify the default or an additional profile. The profile, inbound destination, and line assignments remain. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DELETE_VOICE_PROFILE_OUTBOUND, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_VOICE_PROFILE_OUTBOUND, payload) def disconnect_whatsapp_business_account( self, input: DisconnectWhatsappBusinessAccountInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Disconnect WhatsApp Business account and numbers\n\nDisconnects every attached WhatsApp sender, then unsubscribes our app and removes the project's business account connection. Photon VoIP lines and the numbers in Meta remain. Requires Idempotency-Key. Poll the returned operation; provider refusals appear as operation failures and retain the account for retry with a new key. New signups are blocked while disconnecting, and existing provisions must finish before this request can be accepted." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DISCONNECT_WHATSAPP_BUSINESS_ACCOUNT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DISCONNECT_WHATSAPP_BUSINESS_ACCOUNT, payload) def get_default_voice_profile( self, input: GetDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> RawResponse[models.VoiceProfile]: "Get default Voice profile\n\nGets the profile currently selected as the project default, including its optional inbound delivery state. Requires platforms:read bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_DEFAULT_VOICE_PROFILE, payload) def get_imessage( self, input: GetProjectImessagePlatformInput - ) -> models.ProjectPlatformSettings | RawResponse[models.ProjectPlatformSettings]: + ) -> RawResponse[models.ProjectPlatformSettings]: "Get project iMessage platform\n\nReports whether the project is on shared or dedicated iMessage lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_PROJECT_IMESSAGE_PLATFORM, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_PROJECT_IMESSAGE_PLATFORM, payload) - def get_operation( - self, input: GetOperationInput - ) -> models.GetOperationResponse | RawResponse[models.GetOperationResponse]: + def get_operation(self, input: GetOperationInput) -> RawResponse[models.GetOperationResponse]: "Get operation\n\nReads one operation using the same operation representation as creation and list. The API includes detail.setupUrl only with platforms:write for this project. This is the polling endpoint every asynchronous request here points its Location at, and it resolves from the moment that request is accepted — an operation is committed before its work is dispatched, so there is no window in which the URL 404s. Poll until `state` is one of `succeeded`, `failed` or `cancelled`, pacing from the Retry-After the accepting response returned. While an email domain waits for DNS, `detail` always contains the manual records and may additionally contain `automaticSetup` with a signed provider URL to open separately. Once the operation has produced a resource, the response carries that resource too, so the poll that finishes is also the one that tells you what you got. `succeeded` means the work is done; billing runs behind it and is not something the caller waits on. Operations are never purged, so a 404 means the id was never this project's. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_OPERATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_OPERATION, payload) def get_project_whatsapp_platform( self, input: GetProjectWhatsappPlatformInput - ) -> models.ProjectPlatformSettings | RawResponse[models.ProjectPlatformSettings]: + ) -> RawResponse[models.ProjectPlatformSettings]: "Get project WhatsApp platform\n\nReports whether the project is on shared or dedicated WhatsApp lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_PROJECT_WHATSAPP_PLATFORM, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_PROJECT_WHATSAPP_PLATFORM, payload) - def get_resource( - self, input: GetResourceInput - ) -> models.Resource | RawResponse[models.Resource]: + def get_resource(self, input: GetResourceInput) -> RawResponse[models.Resource]: "Get resource\n\nReads one resource the project holds. A released number stays readable and reads `retired`, because it remains part of this project's history. A dedicated line given back does NOT: returning it to inventory is what makes it claimable by someone else, so it answers 404 and the operation that returned it is the record that this project once held it. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_RESOURCE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_RESOURCE, payload) def get_sms_line_campaign_assignment( self, input: GetSmsLineCampaignAssignmentInput - ) -> ( - models.GetSmsLineCampaignAssignmentResponse - | RawResponse[models.GetSmsLineCampaignAssignmentResponse] - ): + ) -> RawResponse[models.GetSmsLineCampaignAssignmentResponse]: "Read SMS line campaign assignment\n\nRead the last confirmed campaign and current eligibility. Follow changes through their operations. Eligibility is a control-plane assessment, not a delivery or recipient-consent guarantee." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_SMS_LINE_CAMPAIGN_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_SMS_LINE_CAMPAIGN_ASSIGNMENT, payload) def get_voice_line_profile_assignment( self, input: GetVoiceLineProfileAssignmentInput - ) -> models.VoiceLineProfileAssignment | RawResponse[models.VoiceLineProfileAssignment]: + ) -> RawResponse[models.VoiceLineProfileAssignment]: "Get Voice line profile assignment\n\nGets the explicit additional-profile override for an owned Voice-capable line. A line following the project default returns 200 without profileId. Requires platforms:read bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_VOICE_LINE_PROFILE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_VOICE_LINE_PROFILE_ASSIGNMENT, payload) - def get_voice_profile( - self, input: GetVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + def get_voice_profile(self, input: GetVoiceProfileInput) -> RawResponse[models.VoiceProfile]: "Get Voice profile\n\nGets one reusable Voice profile, including its optional inbound delivery state. Requires platforms:read bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_VOICE_PROFILE, payload) def get_whatsapp_business_account( self, input: GetWhatsappBusinessAccountInput - ) -> models.WhatsappBusinessAccount | RawResponse[models.WhatsappBusinessAccount]: + ) -> RawResponse[models.WhatsappBusinessAccount]: "Get WhatsApp Business account\n\nGets the one WhatsApp Business Account this project has connected, with its number of live senders. Senders are resources and are listed by GET /platforms/resources?ability=whatsapp_business. The account is not a resource and carries no access token. Meta's retained numbers are listed separately by GET /platforms/whatsapp-business/account/phone-numbers. `subscribedAt` is absent until our app is attached to the account's webhooks. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WHATSAPP_BUSINESS_ACCOUNT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WHATSAPP_BUSINESS_ACCOUNT, payload) def get_whatsapp_business_verification_code( self, input: GetWhatsappBusinessVerificationCodeInput - ) -> ( - models.GetWhatsappBusinessVerificationCodeResponse - | RawResponse[models.GetWhatsappBusinessVerificationCodeResponse] - ): + ) -> RawResponse[models.GetWhatsappBusinessVerificationCodeResponse]: "Get WhatsApp Business verification code\n\nReturns the latest six-digit WhatsApp Business ownership code received by SMS for an active Photon VOIP number, but only when its provider timestamp is strictly newer than the required receivedAfter boundary. receivedAfter must be an RFC 3339 timestamp between this request's arrival time and two minutes before it; once it expires, restart Meta's verification flow with a new boundary. A missing newer code is a retryable 404 with Retry-After: 2. Poll after 2, 4, 8, then 10 seconds, applying ±20% jitter and capping later intervals at 10 seconds. Stop when the original boundary is two minutes old. Responses are never cached. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WHATSAPP_BUSINESS_VERIFICATION_CODE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WHATSAPP_BUSINESS_VERIFICATION_CODE, payload) def get_whatsapp_shared_line_assignment( self, input: GetWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> RawResponse[models.SharedLineAssignment]: "Get WhatsApp shared line assignment\n\nReads one WhatsApp shared line assignment. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) def get_whatsapp_signup_config( self, input: GetWhatsappSignupConfigInput - ) -> ( - models.GetWhatsappSignupConfigResponse | RawResponse[models.GetWhatsappSignupConfigResponse] - ): + ) -> RawResponse[models.GetWhatsappSignupConfigResponse]: "Get WhatsApp signup config\n\nReturns what the browser needs to open Meta's Embedded Signup popup: the Facebook Login for Business configuration id, the Graph version to run against, and the scopes it will request. Answered in-process rather than forwarded, so the first step of onboarding survives an outage of the private service. Pass `configId` to `FB.login` as `config_id` with `response_type: 'code'` and `override_default_response_type: true`. Do NOT add a `featureType` — omitting it is what keeps the phone-number screen in the flow, and `only_waba_sharing` produces an account with no number that cannot be provisioned. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WHATSAPP_SIGNUP_CONFIG, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WHATSAPP_SIGNUP_CONFIG, payload) def list_number_area_codes( self, input: ListNumberAreaCodesInput - ) -> models.ListNumberAreaCodesResponse | RawResponse[models.ListNumberAreaCodesResponse]: + ) -> RawResponse[models.ListNumberAreaCodesResponse]: "List supported number area codes\n\nLists current provider coverage for US local numbers, sorted and deduplicated. Coverage does not guarantee inventory carrying every required feature. New area-specific purchases must use a listed code; accepted purchases keep waiting if coverage later changes. Requires platforms:read for the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_NUMBER_AREA_CODES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_NUMBER_AREA_CODES, payload) def list_number_countries( self, input: ListNumberCountriesInput - ) -> models.ListNumberCountriesResponse | RawResponse[models.ListNumberCountriesResponse]: + ) -> RawResponse[models.ListNumberCountriesResponse]: "List supported number countries\n\nLists supported purchase countries independently of current provider inventory. Requires platforms:read for the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_NUMBER_COUNTRIES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_NUMBER_COUNTRIES, payload) - def list_operations( - self, input: ListOperationsInput - ) -> models.OperationPage | RawResponse[models.OperationPage]: + def list_operations(self, input: ListOperationsInput) -> RawResponse[models.OperationPage]: "List operations\n\nLists the project's operations using the same operation representation as creation and GET. The API includes detail.setupUrl only with platforms:write for this project. Results are oldest first — every provision and release it has ever asked for, including the ones still running. This is the entire in-flight view: a resource only appears once it is real, so nothing half-built shows up in the resource list and nothing in flight is missing from this one. Filter by `resourceId` to get one resource's whole history, which for a pooled line is every tenure this project has had on it. `state` is comma-separated; `type` accepts one operation type and an absent filter means everything, including failed and cancelled operations. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_OPERATIONS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_OPERATIONS, payload) def list_project_platforms( self, input: ListProjectPlatformsInput - ) -> models.ListProjectPlatformsResponse | RawResponse[models.ListProjectPlatformsResponse]: + ) -> RawResponse[models.ListProjectPlatformsResponse]: "List project platforms\n\nLists the platform types available to this project. Every project currently sees the same fixed public contract, answered in-process rather than forwarded, so the list survives an outage of the private service. The project binding exists so that answer can narrow per project without moving the route. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_PROJECT_PLATFORMS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_PROJECT_PLATFORMS, payload) - def list_resources( - self, input: ListResourcesInput - ) -> models.ResourcePage | RawResponse[models.ResourcePage]: + def list_resources(self, input: ListResourcesInput) -> RawResponse[models.ResourcePage]: "List resources\n\nLists everything the project holds, oldest first, whatever kind of thing it is — one endpoint and one id shape for numbers, dedicated lines and whatever ships next. Nothing half-built appears here: a resource exists only once it is real, so anything still being provisioned is an operation rather than a resource with a pending flag. Filter by `type`, by `ability` (which matches only abilities that are currently enabled), and by `state` — comma-separated, and absent means every state, including retired ones. `detail` carries a per-type public view: an SMS number's number, a dedicated line's number and whether it is healthy. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_RESOURCES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_RESOURCES, payload) def list_voice_profiles( self, input: ListVoiceProfilesInput - ) -> models.VoiceProfilePage | RawResponse[models.VoiceProfilePage]: + ) -> RawResponse[models.VoiceProfilePage]: "List Voice profiles\n\nLists reusable Voice profiles in this project. Requires platforms:read bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_VOICE_PROFILES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_VOICE_PROFILES, payload) def list_whatsapp_account_phone_numbers( self, input: ListWhatsappAccountPhoneNumbersInput - ) -> ( - models.ListWhatsappAccountPhoneNumbersResponse - | RawResponse[models.ListWhatsappAccountPhoneNumbersResponse] - ): + ) -> RawResponse[models.ListWhatsappAccountPhoneNumbersResponse]: "List WhatsApp account phone numbers\n\nLists the connected WABA's phone numbers directly from Meta, including numbers whose Photon sender was disconnected. Ownership is photon for a number in this project's current Photon inventory and meta otherwise. Match a Photon SMS number by its E.164 phoneNumber and reuse its existing displayName when reconnecting. A null name is unavailable, not permission to choose a new name. A failed lookup returns an error rather than an empty list. Requires platforms:read on the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WHATSAPP_ACCOUNT_PHONE_NUMBERS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WHATSAPP_ACCOUNT_PHONE_NUMBERS, payload) def list_whatsapp_shared_line_assignments( self, input: ListWhatsappSharedLineAssignmentsInput - ) -> models.SharedLineAssignmentPage | RawResponse[models.SharedLineAssignmentPage]: + ) -> RawResponse[models.SharedLineAssignmentPage]: "List WhatsApp shared line assignments\n\nLists the project's WhatsApp shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WHATSAPP_SHARED_LINE_ASSIGNMENTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WHATSAPP_SHARED_LINE_ASSIGNMENTS, payload) def provision_imessage_dedicated_line( self, input: ProvisionImessageDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Provision dedicated iMessage line\n\nClaims one dedicated iMessage line for the project and enables iMessage on it. Always answers 202 with an operation: dedicated lines are allocated from available capacity, and unavailable capacity causes a wait rather than a failure — this can legitimately stay `running` for hours, which is exactly why the response is a handle to poll rather than a number. The project's messaging subscription must grant the dedicated iMessage lines entitlement (`imessage_dedicated_lines.can_purchase`), and that is checked before capacity is reserved; nothing is charged until a line is actually claimed. If you no longer want to wait, POST to the operation's cancel endpoint, which costs nothing. The Idempotency-Key is required and permanent: repeating it returns the same operation forever. A further line always needs a NEW key, including while others are still waiting. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_PROVISION_IMESSAGE_DEDICATED_LINE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_PROVISION_IMESSAGE_DEDICATED_LINE, payload) def provision_whatsapp_dedicated_line( self, input: ProvisionWhatsappDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Provision dedicated WhatsApp line\n\nProvisions one dedicated WhatsApp line with WhatsApp and shared Voice enabled. It attaches to an eligible iMessage line the project already owns when possible so both products keep the same number; otherwise it claims healthy, available WhatsApp-capable dedicated-line inventory. Always answers 202, because waiting when no inventory is available is not a failure. The product opens its own charge period after the abilities are enabled; Voice has no separate charge. Cancel the returned operation to stop waiting. The Idempotency-Key is required and permanent." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_PROVISION_WHATSAPP_DEDICATED_LINE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_PROVISION_WHATSAPP_DEDICATED_LINE, payload) - def purchase_sms_number( - self, input: PurchaseSmsNumberInput - ) -> models.Operation | RawResponse[models.Operation]: + def purchase_sms_number(self, input: PurchaseSmsNumberInput) -> RawResponse[models.Operation]: "Purchase SMS number\n\nBuys one US local number from the provider and records it as a resource with SMS enabled. Requires countryCode (US) and accepts an optional three-digit geographic areaCode. The server selects an exact matching number. Empty inventory keeps the operation running until a number is available or the caller cancels before ordering begins. Always answers 202 with an operation: the work runs behind the response, and the Location points at the operation to poll. New area-specific requests must appear in current provider coverage; discover it with GET /sms/numbers/area-codes?countryCode=US. Coverage and subscription checks run before operation creation. Billing follows delivery. Replays return the original operation without checking current coverage. The Idempotency-Key is required and permanent: repeating it returns the same operation forever, never a second number. A further number always needs a NEW key, including while others are still running. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_PURCHASE_SMS_NUMBER, payload) - return response if self._raw else response.data + return self._transport.request(_OP_PURCHASE_SMS_NUMBER, payload) def release_imessage_dedicated_line( self, input: ReleaseImessageDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Release dedicated iMessage line\n\nRemoves only iMessage from one dedicated line. Shared Voice is removed only when WhatsApp is absent; if WhatsApp remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open iMessage charge period identifies this product tenure." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RELEASE_IMESSAGE_DEDICATED_LINE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RELEASE_IMESSAGE_DEDICATED_LINE, payload) - def release_resource( - self, input: ReleaseResourceInput - ) -> models.Operation | RawResponse[models.Operation]: + def release_resource(self, input: ReleaseResourceInput) -> RawResponse[models.Operation]: "Release resource\n\nGives one resource back, whatever it is. What that means is the resource's own business: an SMS number goes back to the provider and is retired, a dedicated iMessage line goes back to the shared pool and stays in existence for someone else to claim. Either way the provider is contacted first where there is one, then a single transaction disables every ability, ends the project's hold and closes the charge period — so a provider that refuses leaves the resource exactly as it was, still owned and still billed. Usually finishes inside this request and answers 200; if the provider is slow it answers 202 and the Location points at the operation to poll. The decrement runs behind the answer either way, so the resource is gone when you are told it is. Takes no Idempotency-Key — releasing the same resource twice is the same request. Releasing one that is already gone answers 404. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RELEASE_RESOURCE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RELEASE_RESOURCE, payload) def release_whatsapp_dedicated_line( self, input: ReleaseWhatsappDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Release dedicated WhatsApp line\n\nRemoves only WhatsApp from one dedicated line. Shared Voice is removed only when iMessage is absent; if iMessage remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open WhatsApp charge period identifies this product tenure." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RELEASE_WHATSAPP_DEDICATED_LINE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RELEASE_WHATSAPP_DEDICATED_LINE, payload) def release_whatsapp_shared_line_assignment( self, input: ReleaseWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> RawResponse[models.SharedLineAssignment]: "Release WhatsApp shared line assignment\n\nReleases a WhatsApp shared line assignment, freeing its seat for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RELEASE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RELEASE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) def replace_voice_profile_inbound( self, input: ReplaceVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: "Create or replace Voice profile inbound configuration\n\nCreates or fully replaces a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. Credentials are required and nullable; null removes destination authentication. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REPLACE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REPLACE_VOICE_PROFILE_INBOUND, payload) def rotate_voice_profile_outbound_credential( self, input: RotateVoiceProfileOutboundCredentialInput - ) -> ( - models.RotateVoiceProfileOutboundCredentialResponse - | RawResponse[models.RotateVoiceProfileOutboundCredentialResponse] - ): + ) -> RawResponse[models.RotateVoiceProfileOutboundCredentialResponse]: "Rotate Voice outbound credential\n\nRotates a SIP profile's outbound credential when expectedVersion matches. The profileId may identify the default or an additional profile. Normal rotation gives the previous credential one hour of grace; emergency rotation gives none. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_ROTATE_VOICE_PROFILE_OUTBOUND_CREDENTIAL, payload) - return response if self._raw else response.data + return self._transport.request(_OP_ROTATE_VOICE_PROFILE_OUTBOUND_CREDENTIAL, payload) def unassign_sms_line_campaign( self, input: UnassignSmsLineCampaignInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> RawResponse[models.Operation]: "Remove SMS line campaign\n\nAny project writer, including a scoped API key, may detach the campaign. The number and campaign remain owned. Requires a permanent Idempotency-Key and expectedVersion. Local eligibility is blocked immediately; provider detachment runs asynchronously." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UNASSIGN_SMS_LINE_CAMPAIGN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UNASSIGN_SMS_LINE_CAMPAIGN, payload) def unassign_voice_line_profile( self, input: UnassignVoiceLineProfileInput - ) -> None | RawResponse[None]: + ) -> RawResponse[None]: "Unassign Voice line profile\n\nRemoves a line's explicit override when the resource version matches so the line follows the project default. Profiles and the pstn_voice ability are unchanged. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UNASSIGN_VOICE_LINE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UNASSIGN_VOICE_LINE_PROFILE, payload) def update_default_voice_profile( self, input: UpdateDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> RawResponse[models.VoiceProfile]: "Update default Voice profile\n\nPatches the default profile's protocol or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Its server-assigned name is immutable, and directional configuration uses the profileId returned by this resource. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_DEFAULT_VOICE_PROFILE, payload) def update_voice_profile( self, input: UpdateVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> RawResponse[models.VoiceProfile]: "Update Voice profile\n\nPatches an additional profile's name, protocol, or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Directional configuration is managed through the profile's inbound and outbound endpoints. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_VOICE_PROFILE, payload) def update_voice_profile_inbound( self, input: UpdateVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: "Update Voice profile inbound configuration\n\nUpdates selected fields of a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. At least one of destinationUri or credentials is required. Credential omission preserves destination authentication, null removes it, and an object replaces it atomically. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_VOICE_PROFILE_INBOUND, payload) def update_voice_profile_outbound_authentication( self, input: UpdateVoiceProfileOutboundAuthenticationInput - ) -> ( - models.UpdateVoiceProfileOutboundAuthenticationResponse - | RawResponse[models.UpdateVoiceProfileOutboundAuthenticationResponse] - ): + ) -> RawResponse[models.UpdateVoiceProfileOutboundAuthenticationResponse]: "Update Voice profile outbound authentication policy\n\nChanges a SIP profile's outbound Digest algorithm when expectedVersion matches. The profileId may identify the default or an additional profile. This policy-only change preserves the password, username, and any previous-password grace deadline. SHA-256 is recommended; MD5 is a weaker legacy option. Returns non-secret outbound metadata and the profile version. Requires platforms:write bound to the path project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request( - _OP_UPDATE_VOICE_PROFILE_OUTBOUND_AUTHENTICATION, payload - ) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_VOICE_PROFILE_OUTBOUND_AUTHENTICATION, payload) -class SyncProjectsAgentProfileResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsAgentProfileResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def commit_avatar( self, input: CommitAgentProfileAvatarInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + ) -> RawResponse[models.AgentProfile]: "Commit an agent avatar\n\nCommits an agent avatar previously uploaded through createAgentProfileAvatarUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's Project ownership, size, content type, image bytes, dimensions, encryption, and age before changing the agent profile." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_COMMIT_AGENT_PROFILE_AVATAR, payload) - return response if self._raw else response.data + return self._transport.request(_OP_COMMIT_AGENT_PROFILE_AVATAR, payload) def create_avatar_upload( self, input: CreateAgentProfileAvatarUploadInput - ) -> models.AgentProfileAvatarUpload | RawResponse[models.AgentProfileAvatarUpload]: + ) -> RawResponse[models.AgentProfileAvatarUpload]: "Create an agent avatar upload\n\nCreates a ten-minute, Project-bound presigned S3 POST for a JPEG, PNG, or WebP agent avatar up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAgentProfileAvatar with the returned uploadId. Do not cache or log the upload URL or form fields." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_AGENT_PROFILE_AVATAR_UPLOAD, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_AGENT_PROFILE_AVATAR_UPLOAD, payload) - def get( - self, input: GetAgentProfileInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def get(self, input: GetAgentProfileInput) -> RawResponse[models.AgentProfile]: "Get an agent profile\n\nReturns the agent profile belonging to the identified project. The profile is project-scoped and is distinct from the authenticated account's personal profile. Use the dedicated avatar operations when uploading or removing an agent avatar." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_AGENT_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_AGENT_PROFILE, payload) - def reset_avatar( - self, input: ResetAgentProfileAvatarInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def reset_avatar(self, input: ResetAgentProfileAvatarInput) -> RawResponse[models.AgentProfile]: "Reset an agent avatar\n\nReplaces the selected project's agent avatar with the project's default avatar, a generated planet image derived from the project ID, and returns the updated agent profile. The reset does not restore an earlier avatar: a custom avatar it replaces is discarded and must be uploaded and committed again to use it. When the default avatar is already in use, the profile is returned unchanged. This does not change the account's personal profile picture. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RESET_AGENT_PROFILE_AVATAR, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RESET_AGENT_PROFILE_AVATAR, payload) - def update( - self, input: UpdateAgentProfileInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def update(self, input: UpdateAgentProfileInput) -> RawResponse[models.AgentProfile]: "Update an agent profile\n\nUpdates the supplied firstName and lastName fields in the project's agent profile and returns the updated profile. Avatar upload, commit and reset are separate operations. The caller must be authorized to change configuration for the selected project. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_AGENT_PROFILE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_AGENT_PROFILE, payload) -class SyncProjectsBillingResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsBillingResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def get_operation( self, input: GetBillingOperationInput - ) -> models.BillingOperation | RawResponse[models.BillingOperation]: + ) -> RawResponse[models.BillingOperation]: "Get a billing operation snapshot\n\nReturns the authoritative state of a billing operation belonging to the selected project. Use it to recover or poll a plan-change request until the operation reaches success or failure. An accepted request is not evidence that the plan change has completed." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_BILLING_OPERATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_BILLING_OPERATION, payload) def get_overview( self, input: GetBillingOverviewInput - ) -> models.GetBillingOverviewResponse | RawResponse[models.GetBillingOverviewResponse]: + ) -> RawResponse[models.GetBillingOverviewResponse]: "Get the project's billing overview\n\nReturns the selected project's plan information, entitlements and current billing-period usage. This operation reads project billing state; it does not change plans or the payer's payment method. Organization-level plans are available through the organization billing overview." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_BILLING_OVERVIEW, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_BILLING_OVERVIEW, payload) def list_billing_plans( self, input: ListBillingPlansInput - ) -> models.ListBillingPlansResponse | RawResponse[models.ListBillingPlansResponse]: + ) -> RawResponse[models.ListBillingPlansResponse]: "List available billing plans\n\nLists the billing plan catalog, grouped by their public plan-metadata type. Use the returned plan information when choosing the category and planCode for a plan change. The catalog is the same for every project, and reading it does not purchase a plan." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_BILLING_PLANS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_BILLING_PLANS, payload) -class SyncProjectsResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawProjectsResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw - self.agent_profile = SyncProjectsAgentProfileResource(transport, raw) - self.billing = SyncProjectsBillingResource(transport, raw) - self.platforms = SyncProjectsPlatformsResource(transport, raw) + self.agent_profile = SyncRawProjectsAgentProfileResource(transport) + self.billing = SyncRawProjectsBillingResource(transport) + self.platforms = SyncRawProjectsPlatformsResource(transport) def create_project_api_key( self, input: CreateProjectApiKeyInput - ) -> models.CreateProjectApiKeyResponse | RawResponse[models.CreateProjectApiKeyResponse]: + ) -> RawResponse[models.CreateProjectApiKeyResponse]: "Create a project API key\n\nCreates a key bound to the selected project using the supplied name, permissions and optional expiry. The secret is returned only in this response and in idempotent replays of it; store it securely because other reads never return it. The key is scoped to this project and does not grant account-level access. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_PROJECT_API_KEY, payload) def create_webhook_destination( self, input: CreateWebhookDestinationInput - ) -> ( - models.CreateWebhookDestinationResponse - | RawResponse[models.CreateWebhookDestinationResponse] - ): + ) -> RawResponse[models.CreateWebhookDestinationResponse]: "Create a webhook destination\n\nCreates a webhook destination for the selected project using its URL, payload API version, event selection and other documented settings. The response includes the signing secret, which is returned only in this response and in idempotent replays of it, never by destination reads; store it securely for signature verification. The API version must be selectable and selected event types must belong to that version's catalog. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_WEBHOOK_DESTINATION, payload) - def delete(self, input: DeleteProjectInput) -> models.Project | RawResponse[models.Project]: + def delete(self, input: DeleteProjectInput) -> RawResponse[models.Project]: "Delete a project\n\nStarts deletion of the identified project using a credential authorized for project management. Inspect the documented response and use getProjectClosureStatus with the organization and project identifiers to read closure progress. A project API key is not an accepted credential for this operation." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DELETE_PROJECT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_PROJECT, payload) def delete_webhook_destination( self, input: DeleteWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> RawResponse[models.WebhookDestination]: "Delete a webhook destination\n\nDeletes the selected project's destination and returns its stable tombstone. Repeated deletion returns the deletion representation. This operation removes the destination configuration; it is separate from disabling a destination through an update." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DELETE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_WEBHOOK_DESTINATION, payload) - def download_attachment(self, input: DownloadAttachmentInput) -> bytes | RawResponse[bytes]: + def download_attachment(self, input: DownloadAttachmentInput) -> RawResponse[bytes]: "Download an Attachment\n\nDownloads an Attachment's bytes. If unavailable after ten seconds, returns ATTACHMENT_NOT_READY with Retry-After: 5." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DOWNLOAD_ATTACHMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DOWNLOAD_ATTACHMENT, payload) - def get(self, input: GetProjectInput) -> models.Project | RawResponse[models.Project]: + def get(self, input: GetProjectInput) -> RawResponse[models.Project]: "Get a project\n\nReturns the identified project's settings for an authorized caller. The credential must be allowed to access that project; possession of an unrelated project's key does not provide access. Missing and deleted projects are reported through the documented error responses." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_PROJECT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_PROJECT, payload) - def get_attachment( - self, input: GetAttachmentInput - ) -> models.Attachment | RawResponse[models.Attachment]: + def get_attachment(self, input: GetAttachmentInput) -> RawResponse[models.Attachment]: "Get an Attachment\n\nReturns an Attachment's metadata. Use the content endpoint to download its bytes." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_ATTACHMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ATTACHMENT, payload) def get_message_metrics_backfill( self, input: GetMessageMetricsBackfillInput - ) -> ( - models.GetMessageMetricsBackfillResponse - | RawResponse[models.GetMessageMetricsBackfillResponse] - ): + ) -> RawResponse[models.GetMessageMetricsBackfillResponse]: "Get Metrics historical backfill status\n\nReturns historical metrics update progress. Completion reflects lastVerifiedAt; queries remain available during updates." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_MESSAGE_METRICS_BACKFILL, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_MESSAGE_METRICS_BACKFILL, payload) def get_message_metrics_sql_schema( self, input: GetMessageMetricsSqlSchemaInput - ) -> ( - models.GetMessageMetricsSqlSchemaResponse - | RawResponse[models.GetMessageMetricsSqlSchemaResponse] - ): + ) -> RawResponse[models.GetMessageMetricsSqlSchemaResponse]: "Get messaging and voice metrics SQL schema\n\nReturns the message_events SQL schema, supported queries, and limits for the selected API version." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_MESSAGE_METRICS_SQL_SCHEMA, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_MESSAGE_METRICS_SQL_SCHEMA, payload) def get_webhook_destination( self, input: GetWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> RawResponse[models.WebhookDestination]: "Get a webhook destination\n\nReturns the configuration of one webhook destination belonging to the selected project. Missing or deleted destinations are reported as errors. This read does not disclose the signing secret returned when the destination or a secret rotation was created." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WEBHOOK_DESTINATION, payload) def get_webhook_event_schema( self, input: GetWebhookEventSchemaInput - ) -> models.WebhookEventSchema | None | RawResponse[models.WebhookEventSchema | None]: + ) -> RawResponse[models.WebhookEventSchema | None]: "Get a webhook event schema\n\nReturns the published reader JSON Schema for eventType in the requested webhook apiVersion. Use it to interpret events for that exact payload version. The response media type is application/schema+json; an authorized conditional request may return 304 without a body. Unsupported event/version combinations are rejected." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_WEBHOOK_EVENT_SCHEMA, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_WEBHOOK_EVENT_SCHEMA, payload) - def list_attachments( - self, input: ListAttachmentsInput - ) -> models.AttachmentPage | RawResponse[models.AttachmentPage]: + def list_attachments(self, input: ListAttachmentsInput) -> RawResponse[models.AttachmentPage]: "List Project Attachments\n\nLists the Project's Attachment metadata, with optional time filters." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_ATTACHMENTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_ATTACHMENTS, payload) def list_project_api_keys( self, input: ListProjectApiKeysInput - ) -> models.ListProjectApiKeysResponse | RawResponse[models.ListProjectApiKeysResponse]: + ) -> RawResponse[models.ListProjectApiKeysResponse]: "List project API keys\n\nLists the API keys on the selected project, ordered newest first. Revoked keys are not listed; expired keys stay listed until they are revoked. Entries contain key metadata and permissions, never secret values. Use the returned identifiers to manage an existing key; lost secrets cannot be recovered through this operation." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_PROJECT_API_KEYS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_PROJECT_API_KEYS, payload) def list_webhook_api_versions( self, input: ListWebhookApiVersionsInput - ) -> ( - models.ListWebhookApiVersionsResponse - | None - | RawResponse[models.ListWebhookApiVersionsResponse | None] - ): + ) -> RawResponse[models.ListWebhookApiVersionsResponse | None]: "List webhook API versions\n\nLists the published webhook payload API versions and their lifecycle metadata. The list is the same for every project. Use the selectable indicator when choosing a version for a destination. These payload dates are separate from SDK package versions. An authorized conditional request may return 304 without a response body." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WEBHOOK_API_VERSIONS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WEBHOOK_API_VERSIONS, payload) def list_webhook_destinations( self, input: ListWebhookDestinationsInput - ) -> models.WebhookDestinationPage | RawResponse[models.WebhookDestinationPage]: + ) -> RawResponse[models.WebhookDestinationPage]: "List webhook destinations\n\nReturns a cursor-paginated page of active webhook destinations configured for the selected project. Use pageSize and pageToken to navigate it. The listing returns destination configuration, never signing secrets." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WEBHOOK_DESTINATIONS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WEBHOOK_DESTINATIONS, payload) def list_webhook_egress_addresses( self, input: ListWebhookEgressAddressesInput - ) -> ( - models.ListWebhookEgressAddressesResponse - | None - | RawResponse[models.ListWebhookEgressAddressesResponse | None] - ): + ) -> RawResponse[models.ListWebhookEgressAddressesResponse | None]: "List webhook egress addresses\n\nReturns the public network addresses from which this environment sends webhook deliveries. Use this information when configuring the receiving system's network allowlist. The result is environment-specific and does not describe the API service's ingress addresses." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WEBHOOK_EGRESS_ADDRESSES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WEBHOOK_EGRESS_ADDRESSES, payload) def list_webhook_event_types( self, input: ListWebhookEventTypesInput - ) -> ( - models.ListWebhookEventTypesResponse - | None - | RawResponse[models.ListWebhookEventTypesResponse | None] - ): + ) -> RawResponse[models.ListWebhookEventTypesResponse | None]: "List webhook event types\n\nLists the webhook event types available in the requested apiVersion, including their descriptions, audiences and reader-schema URLs. Use this versioned catalog when selecting a destination's enabledEvents. The response may include version-retirement information; an authorized conditional request can return 304 without a body." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_WEBHOOK_EVENT_TYPES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_WEBHOOK_EVENT_TYPES, payload) def query_message_metrics( self, input: QueryMessageMetricsInput - ) -> models.QueryMessageMetricsResponse | RawResponse[models.QueryMessageMetricsResponse]: + ) -> RawResponse[models.QueryMessageMetricsResponse]: "Query messaging and voice metrics with SQL\n\nRuns read-only SQL over the Project's message_events table. Get the SQL schema for supported columns, capabilities, and limits." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_QUERY_MESSAGE_METRICS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_QUERY_MESSAGE_METRICS, payload) def revoke_project_api_key( self, input: RevokeProjectApiKeyInput - ) -> models.ProjectApiKeyResponse | RawResponse[models.ProjectApiKeyResponse]: + ) -> RawResponse[models.ProjectApiKeyResponse]: "Delete a project API key\n\nRevokes the identified key on the selected project and returns its revoked metadata. Repeating the deletion returns the same revokedAt value. This operation does not rotate the key or return a replacement secret. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REVOKE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REVOKE_PROJECT_API_KEY, payload) def rotate_webhook_signing_secret( self, input: RotateWebhookSigningSecretInput - ) -> ( - models.RotateWebhookSigningSecretResponse - | RawResponse[models.RotateWebhookSigningSecretResponse] - ): + ) -> RawResponse[models.RotateWebhookSigningSecretResponse]: "Rotate a webhook signing secret\n\nRotates the signing secret for the selected project's webhook destination and returns the new secret. The optional overlapSeconds controls the requested overlap with the previous secret according to the documented request constraints. Store the new secret securely and update the receiver's signature verification configuration; it is returned only in this response and in idempotent replays of it, never by destination reads. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_ROTATE_WEBHOOK_SIGNING_SECRET, payload) - return response if self._raw else response.data + return self._transport.request(_OP_ROTATE_WEBHOOK_SIGNING_SECRET, payload) - def update(self, input: UpdateProjectInput) -> models.Project | RawResponse[models.Project]: + def update(self, input: UpdateProjectInput) -> RawResponse[models.Project]: "Update a project\n\nUpdates the identified project's name and returns the updated project. The project slug is not a mutable field in this request. Use an authorized account or organization service-identity credential; a project API key is not accepted. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_PROJECT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_PROJECT, payload) def update_project_api_key( self, input: UpdateProjectApiKeyInput - ) -> models.ProjectApiKeyResponse | RawResponse[models.ProjectApiKeyResponse]: + ) -> RawResponse[models.ProjectApiKeyResponse]: "Update a project API key's permissions\n\nReplaces the identified project key's permission list with the supplied permissions and returns the updated metadata. Sending the permission list the key already has leaves it unchanged. This request does not create a new secret or change the key's project binding. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_PROJECT_API_KEY, payload) def update_webhook_destination( self, input: UpdateWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> RawResponse[models.WebhookDestination]: "Update a webhook destination\n\nUpdates the supplied URL, name, description, status or enabledEvents fields on a project's webhook destination and returns its updated configuration. The payload API version is not a mutable field in this request. Event selections are checked against the destination's versioned catalog; signing-secret rotation is a separate operation. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_WEBHOOK_DESTINATION, payload) - def upload_attachment( - self, input: UploadAttachmentInput - ) -> models.Attachment | RawResponse[models.Attachment]: + def upload_attachment(self, input: UploadAttachmentInput) -> RawResponse[models.Attachment]: "Upload an Attachment\n\nUploads a file and returns its Attachment once ready to download." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True, exclude={"body"}) payload["body"] = input.body.root if input.body is not None else None - response = self._transport.request(_OP_UPLOAD_ATTACHMENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPLOAD_ATTACHMENT, payload) -class SyncAuthDeviceResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawAuthDeviceResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def authorize( self, input: DeviceAuthorizeInput | None = None - ) -> models.DeviceAuthorizeResponse | RawResponse[models.DeviceAuthorizeResponse]: + ) -> RawResponse[models.DeviceAuthorizeResponse]: "Start Device Authorization\n\nStarts the device authorization flow for a CLI or another device without a browser. Show the verification URL and user code, then poll the token endpoint at the returned interval. No request fields are required; any supplied body is ignored." payload = (input or DeviceAuthorizeInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_DEVICE_AUTHORIZE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DEVICE_AUTHORIZE, payload) - def token( - self, input: DeviceTokenInput - ) -> models.DeviceTokenResponse | RawResponse[models.DeviceTokenResponse]: + def token(self, input: DeviceTokenInput) -> RawResponse[models.DeviceTokenResponse]: "Exchange Device Code or Refresh Token\n\nExchanges an authorized device code or a refresh token for an access token and rotating refresh token. Accepts JSON and form-encoded bodies. While polling, wait at least interval seconds and increase the interval on slow_down. Store the new refresh token after every successful grant.\n\nThis SDK method sends uncompressed JSON (application/json). Other request formats described above apply to direct HTTP requests." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DEVICE_TOKEN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DEVICE_TOKEN, payload) -class SyncAuthResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawAuthResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw - self.device = SyncAuthDeviceResource(transport, raw) + self.device = SyncRawAuthDeviceResource(transport) def begin_invitation_sso( self, input: BeginInvitationSsoInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: "Authenticate to an invitation's organization SSO connection\n\nReturns an authentication URL for the organization SSO connection associated with the supplied invitation token. Supply token and returnTo. Complete the returned authentication flow; requesting its URL does not itself accept the invitation." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_BEGIN_INVITATION_SSO, payload) - return response if self._raw else response.data + return self._transport.request(_OP_BEGIN_INVITATION_SSO, payload) def begin_organization_authentication( self, input: BeginOrganizationAuthenticationInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: "Authenticate to the current organization SSO connection\n\nReturns a URL to authenticate through the selected organization’s current SSO connection. Supply returnTo and open the returned URL to continue the flow. Receiving the URL does not establish an authenticated session." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_BEGIN_ORGANIZATION_AUTHENTICATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_BEGIN_ORGANIZATION_AUTHENTICATION, payload) def begin_organization_closure_authentication( self, input: BeginOrganizationClosureAuthenticationInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: "Authenticate the current Owner to inspect organization closure\n\nReturns an authentication URL for the current organization owner to inspect organization closure. Supply returnTo and complete the returned flow. This operation initiates authentication and does not close the organization." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_BEGIN_ORGANIZATION_CLOSURE_AUTHENTICATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_BEGIN_ORGANIZATION_CLOSURE_AUTHENTICATION, payload) def begin_organization_sso_admission( self, input: BeginOrganizationSsoAdmissionInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: "Begin organization SSO admission for an existing Account\n\nReturns an SSO admission URL for an existing account and the selected organization. Supply returnTo for the continuation URL. Admission requires completing the returned authentication flow; creating the URL does not itself grant membership." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_BEGIN_ORGANIZATION_SSO_ADMISSION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_BEGIN_ORGANIZATION_SSO_ADMISSION, payload) def create_organization_sso_portal_link( self, input: CreateOrganizationSsoPortalLinkInput - ) -> ( - models.CreateOrganizationSsoPortalLinkResponse - | RawResponse[models.CreateOrganizationSsoPortalLinkResponse] - ): + ) -> RawResponse[models.CreateOrganizationSsoPortalLinkResponse]: "Create organization SSO setup portal\n\nReturns an organization setup portal URL. Supply returnTo and optionally intent, either sso or domain_verification; sso is the default. Open the returned URL to complete the selected setup flow." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_ORGANIZATION_SSO_PORTAL_LINK, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_ORGANIZATION_SSO_PORTAL_LINK, payload) def disable_organization_sso( self, input: DisableOrganizationSsoInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> RawResponse[models.OrganizationSsoConfiguration]: "Turn organization SSO off\n\nDeletes the provider connection, releases the SSO requirement once the connection is gone, then unbinds the chosen domains. Retry with the same Idempotency-Key to resume or await the same run." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_DISABLE_ORGANIZATION_SSO, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DISABLE_ORGANIZATION_SSO, payload) def get_organization_connection_status( self, input: GetOrganizationConnectionStatusInput - ) -> models.OrganizationConnectionStatus | RawResponse[models.OrganizationConnectionStatus]: + ) -> RawResponse[models.OrganizationConnectionStatus]: "Read organization and own membership synchronization\n\nRequires current human organization membership. Synchronization status does not attest SSO configuration or completed authorization." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_ORGANIZATION_CONNECTION_STATUS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ORGANIZATION_CONNECTION_STATUS, payload) def get_organization_sso_configuration( self, input: GetOrganizationSsoConfigurationInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> RawResponse[models.OrganizationSsoConfiguration]: "Read organization SSO configuration\n\nReturns the selected organization’s SSO connection state, configuration version, and desired and effective policy settings. Read policySyncStatus alongside the enforcement fields to distinguish requested settings from synchronized settings." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_ORGANIZATION_SSO_CONFIGURATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ORGANIZATION_SSO_CONFIGURATION, payload) def list_oauth_scopes( self, input: ListOauthScopesInput | None = None - ) -> models.ListOauthScopesResponse | RawResponse[models.ListOauthScopesResponse]: + ) -> RawResponse[models.ListOauthScopesResponse]: "List OAuth scopes\n\nLists the business permissions available to OAuth applications." payload = (input or ListOauthScopesInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_LIST_OAUTH_SCOPES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_OAUTH_SCOPES, payload) def refresh_organization_sso_connection( self, input: RefreshOrganizationSsoConnectionInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> RawResponse[models.OrganizationSsoConfiguration]: "Refresh organization SSO connection\n\nRefreshes the selected organization’s SSO connection and returns its current connection state, configuration version and policy synchronization status. This operation takes no request body." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REFRESH_ORGANIZATION_SSO_CONNECTION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REFRESH_ORGANIZATION_SSO_CONNECTION, payload) def retry_organization_connection_sync( self, input: RetryOrganizationConnectionSyncInput - ) -> models.OrganizationConnectionStatus | RawResponse[models.OrganizationConnectionStatus]: + ) -> RawResponse[models.OrganizationConnectionStatus]: "Retry own organization connection synchronization\n\nReconciles existing local intent. Takes no body and cannot change membership, roles or authentication policy." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RETRY_ORGANIZATION_CONNECTION_SYNC, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RETRY_ORGANIZATION_CONNECTION_SYNC, payload) def start_enterprise_login( self, input: StartEnterpriseLoginInput - ) -> models.StartEnterpriseLoginResponse | RawResponse[models.StartEnterpriseLoginResponse]: + ) -> RawResponse[models.StartEnterpriseLoginResponse]: "Start company sign-in without an existing Account\n\nReturns a sign-in URL without requiring an existing account: the company SSO connection when the target has a ready connection, otherwise ordinary account login. Supply one documented enrollment variant: organizationId with returnTo (optionally invitationToken), invitationToken with returnTo, or retryToken. Open the returned URL to continue authentication; receiving a URL does not complete sign-in. This is a browser flow: the request must come from an allowed Origin, and the retryToken variant also needs the retry cookie set by the failed sign-in, so send it with credentials." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_START_ENTERPRISE_LOGIN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_START_ENTERPRISE_LOGIN, payload) def update_organization_sso_policy( self, input: UpdateOrganizationSsoPolicyInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> RawResponse[models.OrganizationSsoConfiguration]: "Update organization SSO policy\n\nUpdates whether SSO can admit new members automatically using ssoJitEnabled and the current expectedVersion. Returns the organization’s SSO configuration and policy synchronization status; a successful response does not mean every desired policy setting has finished synchronizing." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_ORGANIZATION_SSO_POLICY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_ORGANIZATION_SSO_POLICY, payload) -class SyncOrganizationsBillingResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawOrganizationsBillingResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def cancel_subscription( self, input: CancelSubscriptionInput - ) -> models.CancelSubscriptionResponse | RawResponse[models.CancelSubscriptionResponse]: + ) -> RawResponse[models.CancelSubscriptionResponse]: "Cancel a category at the end of its billing period\n\nSchedules cancellation of the specified project's billing category at the end of its current period. The category remains active through the returned cancelsAt instant and then stops renewing. This is a scheduled cancellation, not an immediate removal of the remaining period's service. If the category has no active subscription, nothing changes and the response has cancellationScheduled set to false and cancelsAt set to null. Supply both organizationId and projectId to select the project within its organization." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CANCEL_SUBSCRIPTION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CANCEL_SUBSCRIPTION, payload) def change_plan( self, input: ChangePlanInput - ) -> ( - models.TerminalBillingOperation - | models.PendingBillingOperation - | RawResponse[models.TerminalBillingOperation | models.PendingBillingOperation] - ): + ) -> RawResponse[models.TerminalBillingOperation | models.PendingBillingOperation]: "Purchase or change a category's plan\n\nPurchases or changes the selected project's plan for the supplied category and planCode. A 202 response means the change is pending: poll the returned operation URL and honor Retry-After until it succeeds or fails. A 200 response means the idempotency key resolved to an operation that is already terminal; inspect that result rather than assuming success from the status code alone. Supply the required Idempotency-Key header. Supply both organizationId and projectId to select the project within its organization." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CHANGE_PLAN, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CHANGE_PLAN, payload) def create_organization_payment_method_checkout( self, input: CreateOrganizationPaymentMethodCheckoutInput - ) -> ( - models.CreateOrganizationPaymentMethodCheckoutResponse - | RawResponse[models.CreateOrganizationPaymentMethodCheckoutResponse] - ): + ) -> RawResponse[models.CreateOrganizationPaymentMethodCheckoutResponse]: "Get a payment-method checkout URL for the organization\n\nReturns a hosted payment-method collection URL for the selected organization. An Idempotency-Key header is optional; supply one to make retries safe. Complete the returned checkout flow. Receiving the URL does not mean a card has been saved; check payment-method status afterward." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_ORGANIZATION_PAYMENT_METHOD_CHECKOUT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_ORGANIZATION_PAYMENT_METHOD_CHECKOUT, payload) def create_organization_setup_intent( self, input: CreateOrganizationSetupIntentInput - ) -> ( - models.CreateOrganizationSetupIntentResponse - | RawResponse[models.CreateOrganizationSetupIntentResponse] - ): + ) -> RawResponse[models.CreateOrganizationSetupIntentResponse]: "Create a SetupIntent for an in-app card capture\n\nCreates payment-provider configuration for collecting a card for the selected organization and returns clientSecret and publishableKey. Supply the required Idempotency-Key header. Complete the provider’s card-collection flow separately and avoid logging the returned client secret." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_ORGANIZATION_SETUP_INTENT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_ORGANIZATION_SETUP_INTENT, payload) def get_organization_billing_overview( self, input: GetOrganizationBillingOverviewInput - ) -> ( - models.GetOrganizationBillingOverviewResponse - | RawResponse[models.GetOrganizationBillingOverviewResponse] - ): + ) -> RawResponse[models.GetOrganizationBillingOverviewResponse]: "Get the organization's billing overview\n\nReturns the selected organization’s billing subscription and entitlementsVersion. The subscription can be null. Read the returned plan, charges and entitlements to inspect organization billing; this operation does not purchase or change a plan." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_ORGANIZATION_BILLING_OVERVIEW, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ORGANIZATION_BILLING_OVERVIEW, payload) def get_organization_payment_method( self, input: GetOrganizationPaymentMethodInput - ) -> ( - models.GetOrganizationPaymentMethodResponse - | RawResponse[models.GetOrganizationPaymentMethodResponse] - ): + ) -> RawResponse[models.GetOrganizationPaymentMethodResponse]: "Check the organization for a card on file\n\nReports whether the selected organization has a card on file and returns its documented payment-method metadata. Reading this endpoint does not collect a new card or create a checkout session." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_ORGANIZATION_PAYMENT_METHOD, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ORGANIZATION_PAYMENT_METHOD, payload) - def list_invoices( - self, input: ListInvoicesInput - ) -> models.ListInvoicesResponse | RawResponse[models.ListInvoicesResponse]: + def list_invoices(self, input: ListInvoicesInput) -> RawResponse[models.ListInvoicesResponse]: "List invoices\n\nReturns a single page of the selected organization's invoices; invoices with a zero total are excluded. Use the documented invoice fields to inspect each invoice's billing state. Listing invoices does not make a payment or modify a subscription." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_INVOICES, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_INVOICES, payload) def resume_subscription( self, input: ResumeSubscriptionInput - ) -> models.ResumeSubscriptionResponse | RawResponse[models.ResumeSubscriptionResponse]: + ) -> RawResponse[models.ResumeSubscriptionResponse]: "Resume a category scheduled for cancellation\n\nRemoves a scheduled cancellation for the specified billing category on the selected project so it can renew normally. This operation resumes a category scheduled to cancel; it is separate from purchasing or changing a plan. Supply both organizationId and projectId to select the project within its organization." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RESUME_SUBSCRIPTION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RESUME_SUBSCRIPTION, payload) -class SyncOrganizationsProjectsResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawOrganizationsProjectsResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def check_project_slug_availability( self, input: CheckProjectSlugAvailabilityInput - ) -> ( - models.CheckProjectSlugAvailabilityResponse - | RawResponse[models.CheckProjectSlugAvailabilityResponse] - ): + ) -> RawResponse[models.CheckProjectSlugAvailabilityResponse]: "Check slug availability\n\nReports whether createProject would accept `slug` right now. Advisory: only the create itself allocates, so a caller must still handle SLUG_TAKEN. A malformed slug is rejected on shape; a reserved slug, a slug held by an active project, and a slug retired with a deleted project each answer `available: false` with a reason." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CHECK_PROJECT_SLUG_AVAILABILITY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CHECK_PROJECT_SLUG_AVAILABILITY, payload) - def count( - self, input: CountProjectsInput - ) -> models.ProjectCount | RawResponse[models.ProjectCount]: + def count(self, input: CountProjectsInput) -> RawResponse[models.ProjectCount]: "Count accessible projects\n\nCounts the projects the same filter would list. The count is read from the primary, so it is authoritative rather than replica-lagged." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_COUNT_PROJECTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_COUNT_PROJECTS, payload) - def create(self, input: CreateProjectInput) -> models.Project | RawResponse[models.Project]: + def create(self, input: CreateProjectInput) -> RawResponse[models.Project]: "Create a project\n\nCreates in the authorized organization. In addition to account credentials, explicitly granted Service Identity API keys and M2M tokens may create projects. Project API keys cannot create projects. Creator and private credential evidence come only from the trusted authorization context. The caller-selected slug is immutable, must be 3 to 63 lowercase ASCII alphanumerics separated by single hyphens, and cannot be reserved or held by any active or deleted project." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_PROJECT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_PROJECT, payload) def get_project_closure_status( self, input: GetProjectClosureStatusInput - ) -> ( - models.GetProjectClosureStatusResponse | RawResponse[models.GetProjectClosureStatusResponse] - ): + ) -> RawResponse[models.GetProjectClosureStatusResponse]: "Read project closure progress\n\nReturns closure progress for projectId within organizationId, including deletionOperationId, domain progress and ready. This read operation does not initiate deletion." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_GET_PROJECT_CLOSURE_STATUS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_PROJECT_CLOSURE_STATUS, payload) - def list( - self, input: ListProjectsInput - ) -> models.ProjectPage | RawResponse[models.ProjectPage]: + def list(self, input: ListProjectsInput) -> RawResponse[models.ProjectPage]: "List accessible projects\n\nReturns a cursor-paginated page of the active projects in organizationId. Filter using query and the documented creation-time bounds, and navigate with pageSize and pageToken. Project roles are not returned and role is not a supported filter." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_LIST_PROJECTS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_PROJECTS, payload) -class SyncOrganizationsResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawOrganizationsResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw - self.billing = SyncOrganizationsBillingResource(transport, raw) - self.projects = SyncOrganizationsProjectsResource(transport, raw) + self.billing = SyncRawOrganizationsBillingResource(transport) + self.projects = SyncRawOrganizationsProjectsResource(transport) -class SyncAccountResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawAccountResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def commit_profile_picture( self, input: CommitAccountProfilePictureInput - ) -> models.Account | RawResponse[models.Account]: + ) -> RawResponse[models.Account]: "Commit a profile picture\n\nCommits a profile picture previously uploaded through createAccountProfilePictureUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's ownership, size, content type, image bytes, dimensions, encryption, and age before changing the Account." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_COMMIT_ACCOUNT_PROFILE_PICTURE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_COMMIT_ACCOUNT_PROFILE_PICTURE, payload) def confirm_phone_verification( self, input: ConfirmAccountPhoneVerificationInput - ) -> models.Account | RawResponse[models.Account]: + ) -> RawResponse[models.Account]: "Confirm a phone number verification\n\nBinds the number once the code is approved. Repeat calls return the bound Account." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CONFIRM_ACCOUNT_PHONE_VERIFICATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CONFIRM_ACCOUNT_PHONE_VERIFICATION, payload) def create_account_service_key( self, input: CreateAccountServiceKeyInput - ) -> ( - models.CreateAccountServiceKeyResponse | RawResponse[models.CreateAccountServiceKeyResponse] - ): + ) -> RawResponse[models.CreateAccountServiceKeyResponse]: "Create an Account Service Key\n\nCreates a service key for the authenticated account with the supplied name and optional expiresAt. Returns key metadata and a one-time credential; store the credential securely because it cannot be retrieved through the listing endpoint. These credentials act as the account and must not be distributed as project-scoped keys. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_ACCOUNT_SERVICE_KEY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_ACCOUNT_SERVICE_KEY, payload) def create_profile_picture_upload( self, input: CreateAccountProfilePictureUploadInput - ) -> models.ProfilePictureUpload | RawResponse[models.ProfilePictureUpload]: + ) -> RawResponse[models.ProfilePictureUpload]: "Create a profile picture upload\n\nCreates a ten-minute, Account-bound presigned S3 POST for a JPEG, PNG, or WebP profile picture up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAccountProfilePicture with the returned uploadId. Do not cache or log the upload URL or form fields." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_ACCOUNT_PROFILE_PICTURE_UPLOAD, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_ACCOUNT_PROFILE_PICTURE_UPLOAD, payload) - def delete( - self, input: DeleteAccountInput | None = None - ) -> models.Account | RawResponse[models.Account]: + def delete(self, input: DeleteAccountInput | None = None) -> RawResponse[models.Account]: "Delete the authenticated account\n\nDeletes the authenticated account and returns its account tombstone. The operation is rejected while the account still owns organizations; transfer or close those organizations before retrying. This endpoint acts on the caller's account and does not accept another account's identifier." payload = (input or DeleteAccountInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_DELETE_ACCOUNT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_DELETE_ACCOUNT, payload) - def get( - self, input: GetAccountInput | None = None - ) -> models.Account | RawResponse[models.Account]: + def get(self, input: GetAccountInput | None = None) -> RawResponse[models.Account]: "Get the authenticated account\n\nReturns the profile of the authenticated account. The account is selected from the credential rather than a request parameter. A missing or deleted account is reported as an error instead of an empty profile." payload = (input or GetAccountInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_GET_ACCOUNT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_GET_ACCOUNT, payload) def list_account_service_keys( self, input: ListAccountServiceKeysInput | None = None - ) -> models.ListAccountServiceKeysResponse | RawResponse[models.ListAccountServiceKeysResponse]: + ) -> RawResponse[models.ListAccountServiceKeysResponse]: "List Account Service Keys\n\nReturns metadata for the authenticated account's unrevoked service keys, including expired keys, ordered newest first. Secret values are not returned; a key's credential is disclosed only when that key is created." payload = (input or ListAccountServiceKeysInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_LIST_ACCOUNT_SERVICE_KEYS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_ACCOUNT_SERVICE_KEYS, payload) def list_authorized_applications( self, input: ListAuthorizedApplicationsInput | None = None - ) -> ( - models.ListAuthorizedApplicationsResponse - | RawResponse[models.ListAuthorizedApplicationsResponse] - ): + ) -> RawResponse[models.ListAuthorizedApplicationsResponse]: "List connected applications\n\nLists the OAuth applications authorized by the authenticated user." payload = (input or ListAuthorizedApplicationsInput()).model_dump( mode="json", by_alias=True, exclude_unset=True ) - response = self._transport.request(_OP_LIST_AUTHORIZED_APPLICATIONS, payload) - return response if self._raw else response.data + return self._transport.request(_OP_LIST_AUTHORIZED_APPLICATIONS, payload) def reset_profile_picture( self, input: ResetAccountProfilePictureInput - ) -> models.Account | RawResponse[models.Account]: + ) -> RawResponse[models.Account]: "Remove a profile picture\n\nRemoves the authenticated account's custom profile picture and returns the account using its default picture. This operation does not upload a replacement; use the upload-and-commit operations when setting a new custom picture. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_RESET_ACCOUNT_PROFILE_PICTURE, payload) - return response if self._raw else response.data + return self._transport.request(_OP_RESET_ACCOUNT_PROFILE_PICTURE, payload) def revoke_account_service_key( self, input: RevokeAccountServiceKeyInput - ) -> ( - models.RevokeAccountServiceKeyResponse | RawResponse[models.RevokeAccountServiceKeyResponse] - ): + ) -> RawResponse[models.RevokeAccountServiceKeyResponse]: "Revoke an Account Service Key\n\nRevokes the account-owned service key identified by serviceKeyId and returns its revoked metadata. Repeating the revocation is stable. Revocation changes the credential's validity; it does not create a replacement key. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REVOKE_ACCOUNT_SERVICE_KEY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REVOKE_ACCOUNT_SERVICE_KEY, payload) def revoke_authorized_application( self, input: RevokeAuthorizedApplicationInput - ) -> None | RawResponse[None]: + ) -> RawResponse[None]: "Revoke a connected application\n\nRevokes the authenticated user's grant for one OAuth application." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REVOKE_AUTHORIZED_APPLICATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REVOKE_AUTHORIZED_APPLICATION, payload) def start_phone_verification( self, input: StartAccountPhoneVerificationInput - ) -> ( - models.StartAccountPhoneVerificationResponse - | RawResponse[models.StartAccountPhoneVerificationResponse] - ): + ) -> RawResponse[models.StartAccountPhoneVerificationResponse]: "Start a phone number verification\n\nSends an SMS code. Answers CAPTCHA_REQUIRED with the widget to render when no solved challenge accompanies the request; retry with the returned challengeContext and a token. Rate limited per account, per destination number, and globally; a rejection carries Retry-After." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_START_ACCOUNT_PHONE_VERIFICATION, payload) - return response if self._raw else response.data + return self._transport.request(_OP_START_ACCOUNT_PHONE_VERIFICATION, payload) - def update(self, input: UpdateAccountInput) -> models.Account | RawResponse[models.Account]: + def update(self, input: UpdateAccountInput) -> RawResponse[models.Account]: "Update the authenticated account\n\nUpdates the supplied firstName and lastName fields on the authenticated account and returns the updated profile. Only the documented profile fields can be changed through this endpoint; profile-picture uploads and phone-number verification use their dedicated operations. Supply the required Idempotency-Key header." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_UPDATE_ACCOUNT, payload) - return response if self._raw else response.data + return self._transport.request(_OP_UPDATE_ACCOUNT, payload) -class SyncSystemResource: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: +class SyncRawSystemResource: + def __init__(self, transport: SyncTransport) -> None: self._transport = transport - self._raw = raw def create_app_installation_request( self, input: CreateAppInstallationRequestInput - ) -> ( - models.CreateAppInstallationRequestResponse - | RawResponse[models.CreateAppInstallationRequestResponse] - ): + ) -> RawResponse[models.CreateAppInstallationRequestResponse]: "Request an app installation\n\nAuthenticates a registered app backend using a short-lived signed client assertion. Creates request metadata only; customer approval is still required." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_CREATE_APP_INSTALLATION_REQUEST, payload) - return response if self._raw else response.data + return self._transport.request(_OP_CREATE_APP_INSTALLATION_REQUEST, payload) def redeem_app_installation_delivery( self, input: RedeemAppInstallationDeliveryInput - ) -> ( - models.RedeemAppInstallationDeliveryResponse - | RawResponse[models.RedeemAppInstallationDeliveryResponse] - ): + ) -> RawResponse[models.RedeemAppInstallationDeliveryResponse]: "Redeem an approved installation credential\n\nThe registered app backend authenticates with a signed client assertion and a single-use code. Plaintext is returned only once; retries return status and never create another credential." payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = self._transport.request(_OP_REDEEM_APP_INSTALLATION_DELIVERY, payload) - return response if self._raw else response.data + return self._transport.request(_OP_REDEEM_APP_INSTALLATION_DELIVERY, payload) -class AsyncProjectsPlatformsImessageAssignmentsResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw +class SyncProjectsPlatformsImessageAssignmentsResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsPlatformsImessageAssignmentsResource(transport) - async def create( - self, input: CreateSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + def create(self, input: CreateSharedLineAssignmentInput) -> models.SharedLineAssignment: "Create shared line assignment\n\nMaps an end user's iMessage handle — an E.164 phone number or an email address — onto one of the project's pooled shared iMessage lines, consuming a seat from the project's entitlement. The assigned number is allocated by the server. When an email address is supplied in `email` the user is sent an invite asynchronously to that address; it is never inferred from the handle, and the response never reports whether the send succeeded. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.create(input)).data - async def get( - self, input: GetSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + def get(self, input: GetSharedLineAssignmentInput) -> models.SharedLineAssignment: "Get shared line assignment\n\nReads one shared line assignment. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.get(input)).data - async def list( - self, input: ListSharedLineAssignmentsInput - ) -> models.SharedLineAssignmentPage | RawResponse[models.SharedLineAssignmentPage]: + def list(self, input: ListSharedLineAssignmentsInput) -> models.SharedLineAssignmentPage: "List shared line assignments\n\nLists the project's shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_SHARED_LINE_ASSIGNMENTS, payload) - return response if self._raw else response.data + return (self._raw_resource.list(input)).data - async def release( - self, input: ReleaseSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + def release(self, input: ReleaseSharedLineAssignmentInput) -> models.SharedLineAssignment: "Release shared line assignment\n\nReleases a shared line assignment, freeing both its seat and its handle for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RELEASE_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.release(input)).data -class AsyncProjectsPlatformsImessageResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw - self.assignments = AsyncProjectsPlatformsImessageAssignmentsResource(transport, raw) +class SyncProjectsPlatformsImessageResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsPlatformsImessageResource(transport) + self.assignments = SyncProjectsPlatformsImessageAssignmentsResource(transport) -class AsyncProjectsPlatformsResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw - self.imessage = AsyncProjectsPlatformsImessageResource(transport, raw) +class SyncProjectsPlatformsResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsPlatformsResource(transport) + self.imessage = SyncProjectsPlatformsImessageResource(transport) - async def assign_sms_line_campaign( - self, input: AssignSmsLineCampaignInput - ) -> models.Operation | RawResponse[models.Operation]: + def assign_sms_line_campaign(self, input: AssignSmsLineCampaignInput) -> models.Operation: "Assign or replace SMS line campaign\n\nAttach a ready campaign from this project’s organization to its line. Requires platforms:write for the project; human and machine actors retain their authenticated identity. Requires a permanent Idempotency-Key and the current assignment version. Provider provisioning runs asynchronously." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_ASSIGN_SMS_LINE_CAMPAIGN, payload) - return response if self._raw else response.data + return (self._raw_resource.assign_sms_line_campaign(input)).data - async def assign_voice_line_profile( + def assign_voice_line_profile( self, input: AssignVoiceLineProfileInput - ) -> models.VoiceLineProfileAssignment | RawResponse[models.VoiceLineProfileAssignment]: + ) -> models.VoiceLineProfileAssignment: "Assign Voice line profile\n\nAssigns or replaces a line's explicit additional-profile override when the resource version matches. The current default cannot be assigned explicitly. The pstn_voice ability remains the admission source of truth. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_ASSIGN_VOICE_LINE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.assign_voice_line_profile(input)).data - async def batch_update_voice_line_profile_assignments( + def batch_update_voice_line_profile_assignments( self, input: BatchUpdateVoiceLineProfileAssignmentsInput - ) -> ( - models.BatchUpdateVoiceLineProfileAssignmentsResponse - | RawResponse[models.BatchUpdateVoiceLineProfileAssignmentsResponse] - ): + ) -> models.BatchUpdateVoiceLineProfileAssignmentsResponse: "Batch update Voice line profile assignments\n\nAtomically sets additional-profile overrides or switches lines back to the project default for up to 100 Voice-capable lines. A null profileId means use the default. Every expected resource version must match or no line changes. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_BATCH_UPDATE_VOICE_LINE_PROFILE_ASSIGNMENTS, payload - ) - return response if self._raw else response.data + return (self._raw_resource.batch_update_voice_line_profile_assignments(input)).data - async def cancel_operation( - self, input: CancelOperationInput - ) -> models.Operation | RawResponse[models.Operation]: + def cancel_operation(self, input: CancelOperationInput) -> models.Operation: "Cancel operation\n\nWithdraws a provision that has not been fulfilled yet. This is an operations action rather than a DELETE, because there is nothing to delete: no resource exists until the work commits. Whether it is accepted depends on the resource type — a dedicated iMessage line may sit waiting on inventory for hours and withdrawing costs nothing, while an SMS number is cancellable during inventory waiting and answers 409 once the workflow commits to its first provider order. Campaign assignment and detachment operations cannot be cancelled in any state. Wait for completion before requesting another change; that new change is not a guaranteed rollback. The output-only `cancellable` field is a snapshot; the cancellation transaction always checks the current phase under a row lock. A cancel that loses the race against the work finishing also answers 409: the resource exists and is billed for, so what you want then is to release it. Nothing is charged for a cancelled provision — billing runs after the work, so there is never anything to refund. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CANCEL_OPERATION, payload) - return response if self._raw else response.data + return (self._raw_resource.cancel_operation(input)).data - async def configure_voice_profile_outbound( + def configure_voice_profile_outbound( self, input: ConfigureVoiceProfileOutboundInput - ) -> ( - models.ConfigureVoiceProfileOutboundResponse - | RawResponse[models.ConfigureVoiceProfileOutboundResponse] - ): + ) -> models.ConfigureVoiceProfileOutboundResponse: "Configure Voice profile outbound credential\n\nConfigures a SIP credential for outbound calls from a profile when the shared profile version matches. authentication.algorithm is required: SHA-256 is recommended, while MD5 is a weaker legacy option supported over UDP, TCP, and TLS; TLS is strongly recommended because UDP and TCP do not encrypt SIP signaling. The profileId may identify the default or an additional profile. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CONFIGURE_VOICE_PROFILE_OUTBOUND, payload) - return response if self._raw else response.data + return (self._raw_resource.configure_voice_profile_outbound(input)).data - async def connect_email_domain( - self, input: ConnectEmailDomainInput - ) -> models.Operation | RawResponse[models.Operation]: + def connect_email_domain(self, input: ConnectEmailDomainInput) -> models.Operation: "Connect email domain\n\nReserves a normalized DNS domain and starts its durable email-provider setup. The accepted provision consumes one email-domain entitlement slot until it fails, is cancelled, or becomes a live resource; the plan's email.max_email_domains value sets the project limit. The customer resource does not exist until provider identity and DNS setup reach READY; poll the returned operation for progress. A domain may have only one unfinished provision or live resource globally. Email domains have no additional per-domain charge. The Idempotency-Key is required and permanent: replaying the same key and canonical domain returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CONNECT_EMAIL_DOMAIN, payload) - return response if self._raw else response.data + return (self._raw_resource.connect_email_domain(input)).data - async def connect_telegram_bot( - self, input: ConnectTelegramBotInput - ) -> models.Operation | RawResponse[models.Operation]: + def connect_telegram_bot(self, input: ConnectTelegramBotInput) -> models.Operation: "Connect Telegram bot\n\nStarts a free managed Telegram bot connection. Each project may have one unfinished Telegram provision, including user interaction and failure cleanup. A different Idempotency-Key while one is active returns 409 TELEGRAM_PROVISION_IN_PROGRESS with its operationId and operationUrl; resume it, cancel it while cancellation is available, or wait for it to finish. Rejected keys remain reusable. Open detail.setupUrl to connect an existing managed bot or create a new one with the project's default agent name or a custom display name, then poll Location. The link remains usable while the operation is active and never expires. Replaying the same Idempotency-Key returns the original operation, even after completion or while a newer setup is active. POST, GET and list share the same operation details. The API includes detail.setupUrl only for callers with platforms:write for the project; read-only callers receive the other details unchanged." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CONNECT_TELEGRAM_BOT, payload) - return response if self._raw else response.data + return (self._raw_resource.connect_telegram_bot(input)).data - async def connect_whatsapp_business( - self, input: ConnectWhatsappBusinessInput - ) -> models.Operation | RawResponse[models.Operation]: + def connect_whatsapp_business(self, input: ConnectWhatsappBusinessInput) -> models.Operation: "Connect WhatsApp Business\n\nExchanges the authorization code Embedded Signup returned and connects exactly the selected phone number as one `whatsapp_sender`. Send the WABA id and phone-number id emitted by the same popup attempt; both are treated as selectors and verified against Meta before use. A selected number that matches a non-retired, same-project `voip_line` is linked to it; a number absent from Photon inventory stays unbound; a matching non-retired `cosmos_line`, foreign VoIP line or unassigned VoIP line fails the operation before registration. Connecting is free — no plan requirement — but Billing must report the project's organization as ready with a payment method on file. The Idempotency-Key is required and permanent: replaying the same key returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CONNECT_WHATSAPP_BUSINESS, payload) - return response if self._raw else response.data + return (self._raw_resource.connect_whatsapp_business(input)).data - async def create_default_voice_profile( + def create_default_voice_profile( self, input: CreateDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> models.VoiceProfile: "Create default Voice profile\n\nCreates the project default Voice profile when absent. An identical replay returns the existing default without changing its version; a different existing default conflicts. Direction configuration is managed separately. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.create_default_voice_profile(input)).data - async def create_voice_profile( - self, input: CreateVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + def create_voice_profile(self, input: CreateVoiceProfileInput) -> models.VoiceProfile: "Create Voice profile\n\nCreates a direction-neutral additional Voice profile. The project default must already exist. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.create_voice_profile(input)).data - async def create_whatsapp_shared_line_assignment( + def create_whatsapp_shared_line_assignment( self, input: CreateWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> models.SharedLineAssignment: "Create WhatsApp shared line assignment\n\nMaps an end user's phone number onto one of the project's pooled shared WhatsApp lines, consuming a seat from the project's WhatsApp entitlement. The assigned number is allocated by the server. When an email address is supplied the user is sent an invite asynchronously; the response never reports whether that succeeded. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_CREATE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload - ) - return response if self._raw else response.data + return (self._raw_resource.create_whatsapp_shared_line_assignment(input)).data - async def create_whatsapp_voip_sender( - self, input: CreateWhatsappVoipSenderInput - ) -> models.Operation | RawResponse[models.Operation]: + def create_whatsapp_voip_sender(self, input: CreateWhatsappVoipSenderInput) -> models.Operation: "Create a VoIP-backed WhatsApp sender\n\nRegisters an active, SMS-capable Photon VoIP line on this project's connected WhatsApp Business Account. The account is resolved server-side; callers never select a WABA. The platform creates or reuses the Meta number, requests and consumes the SMS ownership code internally, verifies it, and registers the sender. displayName is optional; when omitted the project agent profile name is snapshotted before acceptance. The VoIP line remains a separate resource and never receives the whatsapp_business ability." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_WHATSAPP_VOIP_SENDER, payload) - return response if self._raw else response.data + return (self._raw_resource.create_whatsapp_voip_sender(input)).data - async def delete_voice_profile( - self, input: DeleteVoiceProfileInput - ) -> None | RawResponse[None]: + def delete_voice_profile(self, input: DeleteVoiceProfileInput) -> None: "Delete Voice profile\n\nDeletes an additional profile when expectedVersion matches. Assigned profiles require force=true, which atomically removes every stored override so affected lines follow the default. The default can never be deleted. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DELETE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.delete_voice_profile(input)).data - async def delete_voice_profile_inbound( + def delete_voice_profile_inbound( self, input: DeleteVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> models.VoiceProfileInboundConfiguration: "Remove Voice profile inbound configuration\n\nRemoves a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. The profile and its line assignments remain. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DELETE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return (self._raw_resource.delete_voice_profile_inbound(input)).data - async def delete_voice_profile_outbound( + def delete_voice_profile_outbound( self, input: DeleteVoiceProfileOutboundInput - ) -> ( - models.DeleteVoiceProfileOutboundResponse - | RawResponse[models.DeleteVoiceProfileOutboundResponse] - ): + ) -> models.DeleteVoiceProfileOutboundResponse: "Revoke Voice profile outbound credential\n\nRevokes outbound calling for a profile when the shared profile version matches. The profileId may identify the default or an additional profile. The profile, inbound destination, and line assignments remain. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DELETE_VOICE_PROFILE_OUTBOUND, payload) - return response if self._raw else response.data + return (self._raw_resource.delete_voice_profile_outbound(input)).data - async def disconnect_whatsapp_business_account( + def disconnect_whatsapp_business_account( self, input: DisconnectWhatsappBusinessAccountInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> models.Operation: "Disconnect WhatsApp Business account and numbers\n\nDisconnects every attached WhatsApp sender, then unsubscribes our app and removes the project's business account connection. Photon VoIP lines and the numbers in Meta remain. Requires Idempotency-Key. Poll the returned operation; provider refusals appear as operation failures and retain the account for retry with a new key. New signups are blocked while disconnecting, and existing provisions must finish before this request can be accepted." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DISCONNECT_WHATSAPP_BUSINESS_ACCOUNT, payload) - return response if self._raw else response.data + return (self._raw_resource.disconnect_whatsapp_business_account(input)).data - async def get_default_voice_profile( - self, input: GetDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + def get_default_voice_profile(self, input: GetDefaultVoiceProfileInput) -> models.VoiceProfile: "Get default Voice profile\n\nGets the profile currently selected as the project default, including its optional inbound delivery state. Requires platforms:read bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.get_default_voice_profile(input)).data - async def get_imessage( + def get_imessage( self, input: GetProjectImessagePlatformInput - ) -> models.ProjectPlatformSettings | RawResponse[models.ProjectPlatformSettings]: + ) -> models.ProjectPlatformSettings: "Get project iMessage platform\n\nReports whether the project is on shared or dedicated iMessage lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_PROJECT_IMESSAGE_PLATFORM, payload) - return response if self._raw else response.data + return (self._raw_resource.get_imessage(input)).data - async def get_operation( - self, input: GetOperationInput - ) -> models.GetOperationResponse | RawResponse[models.GetOperationResponse]: + def get_operation(self, input: GetOperationInput) -> models.GetOperationResponse: "Get operation\n\nReads one operation using the same operation representation as creation and list. The API includes detail.setupUrl only with platforms:write for this project. This is the polling endpoint every asynchronous request here points its Location at, and it resolves from the moment that request is accepted — an operation is committed before its work is dispatched, so there is no window in which the URL 404s. Poll until `state` is one of `succeeded`, `failed` or `cancelled`, pacing from the Retry-After the accepting response returned. While an email domain waits for DNS, `detail` always contains the manual records and may additionally contain `automaticSetup` with a signed provider URL to open separately. Once the operation has produced a resource, the response carries that resource too, so the poll that finishes is also the one that tells you what you got. `succeeded` means the work is done; billing runs behind it and is not something the caller waits on. Operations are never purged, so a 404 means the id was never this project's. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_OPERATION, payload) - return response if self._raw else response.data + return (self._raw_resource.get_operation(input)).data - async def get_project_whatsapp_platform( + def get_project_whatsapp_platform( self, input: GetProjectWhatsappPlatformInput - ) -> models.ProjectPlatformSettings | RawResponse[models.ProjectPlatformSettings]: + ) -> models.ProjectPlatformSettings: "Get project WhatsApp platform\n\nReports whether the project is on shared or dedicated WhatsApp lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_PROJECT_WHATSAPP_PLATFORM, payload) - return response if self._raw else response.data + return (self._raw_resource.get_project_whatsapp_platform(input)).data - async def get_resource( - self, input: GetResourceInput - ) -> models.Resource | RawResponse[models.Resource]: + def get_resource(self, input: GetResourceInput) -> models.Resource: "Get resource\n\nReads one resource the project holds. A released number stays readable and reads `retired`, because it remains part of this project's history. A dedicated line given back does NOT: returning it to inventory is what makes it claimable by someone else, so it answers 404 and the operation that returned it is the record that this project once held it. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_RESOURCE, payload) - return response if self._raw else response.data + return (self._raw_resource.get_resource(input)).data - async def get_sms_line_campaign_assignment( + def get_sms_line_campaign_assignment( self, input: GetSmsLineCampaignAssignmentInput - ) -> ( - models.GetSmsLineCampaignAssignmentResponse - | RawResponse[models.GetSmsLineCampaignAssignmentResponse] - ): + ) -> models.GetSmsLineCampaignAssignmentResponse: "Read SMS line campaign assignment\n\nRead the last confirmed campaign and current eligibility. Follow changes through their operations. Eligibility is a control-plane assessment, not a delivery or recipient-consent guarantee." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_SMS_LINE_CAMPAIGN_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.get_sms_line_campaign_assignment(input)).data - async def get_voice_line_profile_assignment( + def get_voice_line_profile_assignment( self, input: GetVoiceLineProfileAssignmentInput - ) -> models.VoiceLineProfileAssignment | RawResponse[models.VoiceLineProfileAssignment]: + ) -> models.VoiceLineProfileAssignment: "Get Voice line profile assignment\n\nGets the explicit additional-profile override for an owned Voice-capable line. A line following the project default returns 200 without profileId. Requires platforms:read bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_VOICE_LINE_PROFILE_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.get_voice_line_profile_assignment(input)).data - async def get_voice_profile( - self, input: GetVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + def get_voice_profile(self, input: GetVoiceProfileInput) -> models.VoiceProfile: "Get Voice profile\n\nGets one reusable Voice profile, including its optional inbound delivery state. Requires platforms:read bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.get_voice_profile(input)).data - async def get_whatsapp_business_account( + def get_whatsapp_business_account( self, input: GetWhatsappBusinessAccountInput - ) -> models.WhatsappBusinessAccount | RawResponse[models.WhatsappBusinessAccount]: + ) -> models.WhatsappBusinessAccount: "Get WhatsApp Business account\n\nGets the one WhatsApp Business Account this project has connected, with its number of live senders. Senders are resources and are listed by GET /platforms/resources?ability=whatsapp_business. The account is not a resource and carries no access token. Meta's retained numbers are listed separately by GET /platforms/whatsapp-business/account/phone-numbers. `subscribedAt` is absent until our app is attached to the account's webhooks. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_WHATSAPP_BUSINESS_ACCOUNT, payload) - return response if self._raw else response.data + return (self._raw_resource.get_whatsapp_business_account(input)).data - async def get_whatsapp_business_verification_code( + def get_whatsapp_business_verification_code( self, input: GetWhatsappBusinessVerificationCodeInput - ) -> ( - models.GetWhatsappBusinessVerificationCodeResponse - | RawResponse[models.GetWhatsappBusinessVerificationCodeResponse] - ): + ) -> models.GetWhatsappBusinessVerificationCodeResponse: "Get WhatsApp Business verification code\n\nReturns the latest six-digit WhatsApp Business ownership code received by SMS for an active Photon VOIP number, but only when its provider timestamp is strictly newer than the required receivedAfter boundary. receivedAfter must be an RFC 3339 timestamp between this request's arrival time and two minutes before it; once it expires, restart Meta's verification flow with a new boundary. A missing newer code is a retryable 404 with Retry-After: 2. Poll after 2, 4, 8, then 10 seconds, applying ±20% jitter and capping later intervals at 10 seconds. Stop when the original boundary is two minutes old. Responses are never cached. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_GET_WHATSAPP_BUSINESS_VERIFICATION_CODE, payload - ) - return response if self._raw else response.data + return (self._raw_resource.get_whatsapp_business_verification_code(input)).data - async def get_whatsapp_shared_line_assignment( + def get_whatsapp_shared_line_assignment( self, input: GetWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> models.SharedLineAssignment: "Get WhatsApp shared line assignment\n\nReads one WhatsApp shared line assignment. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.get_whatsapp_shared_line_assignment(input)).data - async def get_whatsapp_signup_config( + def get_whatsapp_signup_config( self, input: GetWhatsappSignupConfigInput - ) -> ( - models.GetWhatsappSignupConfigResponse | RawResponse[models.GetWhatsappSignupConfigResponse] - ): + ) -> models.GetWhatsappSignupConfigResponse: "Get WhatsApp signup config\n\nReturns what the browser needs to open Meta's Embedded Signup popup: the Facebook Login for Business configuration id, the Graph version to run against, and the scopes it will request. Answered in-process rather than forwarded, so the first step of onboarding survives an outage of the private service. Pass `configId` to `FB.login` as `config_id` with `response_type: 'code'` and `override_default_response_type: true`. Do NOT add a `featureType` — omitting it is what keeps the phone-number screen in the flow, and `only_waba_sharing` produces an account with no number that cannot be provisioned. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_WHATSAPP_SIGNUP_CONFIG, payload) - return response if self._raw else response.data + return (self._raw_resource.get_whatsapp_signup_config(input)).data - async def list_number_area_codes( + def list_number_area_codes( self, input: ListNumberAreaCodesInput - ) -> models.ListNumberAreaCodesResponse | RawResponse[models.ListNumberAreaCodesResponse]: + ) -> models.ListNumberAreaCodesResponse: "List supported number area codes\n\nLists current provider coverage for US local numbers, sorted and deduplicated. Coverage does not guarantee inventory carrying every required feature. New area-specific purchases must use a listed code; accepted purchases keep waiting if coverage later changes. Requires platforms:read for the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_NUMBER_AREA_CODES, payload) - return response if self._raw else response.data + return (self._raw_resource.list_number_area_codes(input)).data - async def list_number_countries( + def list_number_countries( self, input: ListNumberCountriesInput - ) -> models.ListNumberCountriesResponse | RawResponse[models.ListNumberCountriesResponse]: + ) -> models.ListNumberCountriesResponse: "List supported number countries\n\nLists supported purchase countries independently of current provider inventory. Requires platforms:read for the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_NUMBER_COUNTRIES, payload) - return response if self._raw else response.data + return (self._raw_resource.list_number_countries(input)).data - async def list_operations( - self, input: ListOperationsInput - ) -> models.OperationPage | RawResponse[models.OperationPage]: + def list_operations(self, input: ListOperationsInput) -> models.OperationPage: "List operations\n\nLists the project's operations using the same operation representation as creation and GET. The API includes detail.setupUrl only with platforms:write for this project. Results are oldest first — every provision and release it has ever asked for, including the ones still running. This is the entire in-flight view: a resource only appears once it is real, so nothing half-built shows up in the resource list and nothing in flight is missing from this one. Filter by `resourceId` to get one resource's whole history, which for a pooled line is every tenure this project has had on it. `state` is comma-separated; `type` accepts one operation type and an absent filter means everything, including failed and cancelled operations. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_OPERATIONS, payload) - return response if self._raw else response.data + return (self._raw_resource.list_operations(input)).data - async def list_project_platforms( + def list_project_platforms( self, input: ListProjectPlatformsInput - ) -> models.ListProjectPlatformsResponse | RawResponse[models.ListProjectPlatformsResponse]: + ) -> models.ListProjectPlatformsResponse: "List project platforms\n\nLists the platform types available to this project. Every project currently sees the same fixed public contract, answered in-process rather than forwarded, so the list survives an outage of the private service. The project binding exists so that answer can narrow per project without moving the route. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_PROJECT_PLATFORMS, payload) - return response if self._raw else response.data + return (self._raw_resource.list_project_platforms(input)).data - async def list_resources( - self, input: ListResourcesInput - ) -> models.ResourcePage | RawResponse[models.ResourcePage]: + def list_resources(self, input: ListResourcesInput) -> models.ResourcePage: "List resources\n\nLists everything the project holds, oldest first, whatever kind of thing it is — one endpoint and one id shape for numbers, dedicated lines and whatever ships next. Nothing half-built appears here: a resource exists only once it is real, so anything still being provisioned is an operation rather than a resource with a pending flag. Filter by `type`, by `ability` (which matches only abilities that are currently enabled), and by `state` — comma-separated, and absent means every state, including retired ones. `detail` carries a per-type public view: an SMS number's number, a dedicated line's number and whether it is healthy. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_RESOURCES, payload) - return response if self._raw else response.data + return (self._raw_resource.list_resources(input)).data - async def list_voice_profiles( - self, input: ListVoiceProfilesInput - ) -> models.VoiceProfilePage | RawResponse[models.VoiceProfilePage]: + def list_voice_profiles(self, input: ListVoiceProfilesInput) -> models.VoiceProfilePage: "List Voice profiles\n\nLists reusable Voice profiles in this project. Requires platforms:read bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_VOICE_PROFILES, payload) - return response if self._raw else response.data + return (self._raw_resource.list_voice_profiles(input)).data - async def list_whatsapp_account_phone_numbers( + def list_whatsapp_account_phone_numbers( self, input: ListWhatsappAccountPhoneNumbersInput - ) -> ( - models.ListWhatsappAccountPhoneNumbersResponse - | RawResponse[models.ListWhatsappAccountPhoneNumbersResponse] - ): + ) -> models.ListWhatsappAccountPhoneNumbersResponse: "List WhatsApp account phone numbers\n\nLists the connected WABA's phone numbers directly from Meta, including numbers whose Photon sender was disconnected. Ownership is photon for a number in this project's current Photon inventory and meta otherwise. Match a Photon SMS number by its E.164 phoneNumber and reuse its existing displayName when reconnecting. A null name is unavailable, not permission to choose a new name. A failed lookup returns an error rather than an empty list. Requires platforms:read on the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WHATSAPP_ACCOUNT_PHONE_NUMBERS, payload) - return response if self._raw else response.data + return (self._raw_resource.list_whatsapp_account_phone_numbers(input)).data - async def list_whatsapp_shared_line_assignments( + def list_whatsapp_shared_line_assignments( self, input: ListWhatsappSharedLineAssignmentsInput - ) -> models.SharedLineAssignmentPage | RawResponse[models.SharedLineAssignmentPage]: + ) -> models.SharedLineAssignmentPage: "List WhatsApp shared line assignments\n\nLists the project's WhatsApp shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WHATSAPP_SHARED_LINE_ASSIGNMENTS, payload) - return response if self._raw else response.data + return (self._raw_resource.list_whatsapp_shared_line_assignments(input)).data - async def provision_imessage_dedicated_line( + def provision_imessage_dedicated_line( self, input: ProvisionImessageDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> models.Operation: "Provision dedicated iMessage line\n\nClaims one dedicated iMessage line for the project and enables iMessage on it. Always answers 202 with an operation: dedicated lines are allocated from available capacity, and unavailable capacity causes a wait rather than a failure — this can legitimately stay `running` for hours, which is exactly why the response is a handle to poll rather than a number. The project's messaging subscription must grant the dedicated iMessage lines entitlement (`imessage_dedicated_lines.can_purchase`), and that is checked before capacity is reserved; nothing is charged until a line is actually claimed. If you no longer want to wait, POST to the operation's cancel endpoint, which costs nothing. The Idempotency-Key is required and permanent: repeating it returns the same operation forever. A further line always needs a NEW key, including while others are still waiting. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_PROVISION_IMESSAGE_DEDICATED_LINE, payload) - return response if self._raw else response.data + return (self._raw_resource.provision_imessage_dedicated_line(input)).data - async def provision_whatsapp_dedicated_line( + def provision_whatsapp_dedicated_line( self, input: ProvisionWhatsappDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> models.Operation: "Provision dedicated WhatsApp line\n\nProvisions one dedicated WhatsApp line with WhatsApp and shared Voice enabled. It attaches to an eligible iMessage line the project already owns when possible so both products keep the same number; otherwise it claims healthy, available WhatsApp-capable dedicated-line inventory. Always answers 202, because waiting when no inventory is available is not a failure. The product opens its own charge period after the abilities are enabled; Voice has no separate charge. Cancel the returned operation to stop waiting. The Idempotency-Key is required and permanent." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_PROVISION_WHATSAPP_DEDICATED_LINE, payload) - return response if self._raw else response.data + return (self._raw_resource.provision_whatsapp_dedicated_line(input)).data - async def purchase_sms_number( - self, input: PurchaseSmsNumberInput - ) -> models.Operation | RawResponse[models.Operation]: + def purchase_sms_number(self, input: PurchaseSmsNumberInput) -> models.Operation: "Purchase SMS number\n\nBuys one US local number from the provider and records it as a resource with SMS enabled. Requires countryCode (US) and accepts an optional three-digit geographic areaCode. The server selects an exact matching number. Empty inventory keeps the operation running until a number is available or the caller cancels before ordering begins. Always answers 202 with an operation: the work runs behind the response, and the Location points at the operation to poll. New area-specific requests must appear in current provider coverage; discover it with GET /sms/numbers/area-codes?countryCode=US. Coverage and subscription checks run before operation creation. Billing follows delivery. Replays return the original operation without checking current coverage. The Idempotency-Key is required and permanent: repeating it returns the same operation forever, never a second number. A further number always needs a NEW key, including while others are still running. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_PURCHASE_SMS_NUMBER, payload) - return response if self._raw else response.data + return (self._raw_resource.purchase_sms_number(input)).data - async def release_imessage_dedicated_line( + def release_imessage_dedicated_line( self, input: ReleaseImessageDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> models.Operation: "Release dedicated iMessage line\n\nRemoves only iMessage from one dedicated line. Shared Voice is removed only when WhatsApp is absent; if WhatsApp remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open iMessage charge period identifies this product tenure." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RELEASE_IMESSAGE_DEDICATED_LINE, payload) - return response if self._raw else response.data + return (self._raw_resource.release_imessage_dedicated_line(input)).data - async def release_resource( - self, input: ReleaseResourceInput - ) -> models.Operation | RawResponse[models.Operation]: + def release_resource(self, input: ReleaseResourceInput) -> models.Operation: "Release resource\n\nGives one resource back, whatever it is. What that means is the resource's own business: an SMS number goes back to the provider and is retired, a dedicated iMessage line goes back to the shared pool and stays in existence for someone else to claim. Either way the provider is contacted first where there is one, then a single transaction disables every ability, ends the project's hold and closes the charge period — so a provider that refuses leaves the resource exactly as it was, still owned and still billed. Usually finishes inside this request and answers 200; if the provider is slow it answers 202 and the Location points at the operation to poll. The decrement runs behind the answer either way, so the resource is gone when you are told it is. Takes no Idempotency-Key — releasing the same resource twice is the same request. Releasing one that is already gone answers 404. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RELEASE_RESOURCE, payload) - return response if self._raw else response.data + return (self._raw_resource.release_resource(input)).data - async def release_whatsapp_dedicated_line( + def release_whatsapp_dedicated_line( self, input: ReleaseWhatsappDedicatedLineInput - ) -> models.Operation | RawResponse[models.Operation]: + ) -> models.Operation: "Release dedicated WhatsApp line\n\nRemoves only WhatsApp from one dedicated line. Shared Voice is removed only when iMessage is absent; if iMessage remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open WhatsApp charge period identifies this product tenure." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RELEASE_WHATSAPP_DEDICATED_LINE, payload) - return response if self._raw else response.data + return (self._raw_resource.release_whatsapp_dedicated_line(input)).data - async def release_whatsapp_shared_line_assignment( + def release_whatsapp_shared_line_assignment( self, input: ReleaseWhatsappSharedLineAssignmentInput - ) -> models.SharedLineAssignment | RawResponse[models.SharedLineAssignment]: + ) -> models.SharedLineAssignment: "Release WhatsApp shared line assignment\n\nReleases a WhatsApp shared line assignment, freeing its seat for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_RELEASE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload - ) - return response if self._raw else response.data + return (self._raw_resource.release_whatsapp_shared_line_assignment(input)).data - async def replace_voice_profile_inbound( + def replace_voice_profile_inbound( self, input: ReplaceVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> models.VoiceProfileInboundConfiguration: "Create or replace Voice profile inbound configuration\n\nCreates or fully replaces a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. Credentials are required and nullable; null removes destination authentication. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REPLACE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return (self._raw_resource.replace_voice_profile_inbound(input)).data - async def rotate_voice_profile_outbound_credential( + def rotate_voice_profile_outbound_credential( self, input: RotateVoiceProfileOutboundCredentialInput - ) -> ( - models.RotateVoiceProfileOutboundCredentialResponse - | RawResponse[models.RotateVoiceProfileOutboundCredentialResponse] - ): + ) -> models.RotateVoiceProfileOutboundCredentialResponse: "Rotate Voice outbound credential\n\nRotates a SIP profile's outbound credential when expectedVersion matches. The profileId may identify the default or an additional profile. Normal rotation gives the previous credential one hour of grace; emergency rotation gives none. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_ROTATE_VOICE_PROFILE_OUTBOUND_CREDENTIAL, payload - ) - return response if self._raw else response.data + return (self._raw_resource.rotate_voice_profile_outbound_credential(input)).data - async def unassign_sms_line_campaign( - self, input: UnassignSmsLineCampaignInput - ) -> models.Operation | RawResponse[models.Operation]: + def unassign_sms_line_campaign(self, input: UnassignSmsLineCampaignInput) -> models.Operation: "Remove SMS line campaign\n\nAny project writer, including a scoped API key, may detach the campaign. The number and campaign remain owned. Requires a permanent Idempotency-Key and expectedVersion. Local eligibility is blocked immediately; provider detachment runs asynchronously." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UNASSIGN_SMS_LINE_CAMPAIGN, payload) - return response if self._raw else response.data + return (self._raw_resource.unassign_sms_line_campaign(input)).data - async def unassign_voice_line_profile( - self, input: UnassignVoiceLineProfileInput - ) -> None | RawResponse[None]: + def unassign_voice_line_profile(self, input: UnassignVoiceLineProfileInput) -> None: "Unassign Voice line profile\n\nRemoves a line's explicit override when the resource version matches so the line follows the project default. Profiles and the pstn_voice ability are unchanged. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UNASSIGN_VOICE_LINE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.unassign_voice_line_profile(input)).data - async def update_default_voice_profile( + def update_default_voice_profile( self, input: UpdateDefaultVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + ) -> models.VoiceProfile: "Update default Voice profile\n\nPatches the default profile's protocol or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Its server-assigned name is immutable, and directional configuration uses the profileId returned by this resource. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_DEFAULT_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.update_default_voice_profile(input)).data - async def update_voice_profile( - self, input: UpdateVoiceProfileInput - ) -> models.VoiceProfile | RawResponse[models.VoiceProfile]: + def update_voice_profile(self, input: UpdateVoiceProfileInput) -> models.VoiceProfile: "Update Voice profile\n\nPatches an additional profile's name, protocol, or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Directional configuration is managed through the profile's inbound and outbound endpoints. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_VOICE_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.update_voice_profile(input)).data - async def update_voice_profile_inbound( + def update_voice_profile_inbound( self, input: UpdateVoiceProfileInboundInput - ) -> ( - models.VoiceProfileInboundConfiguration - | RawResponse[models.VoiceProfileInboundConfiguration] - ): + ) -> models.VoiceProfileInboundConfiguration: "Update Voice profile inbound configuration\n\nUpdates selected fields of a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. At least one of destinationUri or credentials is required. Credential omission preserves destination authentication, null removes it, and an object replaces it atomically. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_VOICE_PROFILE_INBOUND, payload) - return response if self._raw else response.data + return (self._raw_resource.update_voice_profile_inbound(input)).data - async def update_voice_profile_outbound_authentication( + def update_voice_profile_outbound_authentication( self, input: UpdateVoiceProfileOutboundAuthenticationInput - ) -> ( - models.UpdateVoiceProfileOutboundAuthenticationResponse - | RawResponse[models.UpdateVoiceProfileOutboundAuthenticationResponse] - ): + ) -> models.UpdateVoiceProfileOutboundAuthenticationResponse: "Update Voice profile outbound authentication policy\n\nChanges a SIP profile's outbound Digest algorithm when expectedVersion matches. The profileId may identify the default or an additional profile. This policy-only change preserves the password, username, and any previous-password grace deadline. SHA-256 is recommended; MD5 is a weaker legacy option. Returns non-secret outbound metadata and the profile version. Requires platforms:write bound to the path project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_UPDATE_VOICE_PROFILE_OUTBOUND_AUTHENTICATION, payload - ) - return response if self._raw else response.data + return (self._raw_resource.update_voice_profile_outbound_authentication(input)).data -class AsyncProjectsAgentProfileResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw +class SyncProjectsAgentProfileResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsAgentProfileResource(transport) - async def commit_avatar( - self, input: CommitAgentProfileAvatarInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def commit_avatar(self, input: CommitAgentProfileAvatarInput) -> models.AgentProfile: "Commit an agent avatar\n\nCommits an agent avatar previously uploaded through createAgentProfileAvatarUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's Project ownership, size, content type, image bytes, dimensions, encryption, and age before changing the agent profile." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_COMMIT_AGENT_PROFILE_AVATAR, payload) - return response if self._raw else response.data + return (self._raw_resource.commit_avatar(input)).data - async def create_avatar_upload( + def create_avatar_upload( self, input: CreateAgentProfileAvatarUploadInput - ) -> models.AgentProfileAvatarUpload | RawResponse[models.AgentProfileAvatarUpload]: + ) -> models.AgentProfileAvatarUpload: "Create an agent avatar upload\n\nCreates a ten-minute, Project-bound presigned S3 POST for a JPEG, PNG, or WebP agent avatar up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAgentProfileAvatar with the returned uploadId. Do not cache or log the upload URL or form fields." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_AGENT_PROFILE_AVATAR_UPLOAD, payload) - return response if self._raw else response.data + return (self._raw_resource.create_avatar_upload(input)).data - async def get( - self, input: GetAgentProfileInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def get(self, input: GetAgentProfileInput) -> models.AgentProfile: "Get an agent profile\n\nReturns the agent profile belonging to the identified project. The profile is project-scoped and is distinct from the authenticated account's personal profile. Use the dedicated avatar operations when uploading or removing an agent avatar." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_AGENT_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.get(input)).data - async def reset_avatar( - self, input: ResetAgentProfileAvatarInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def reset_avatar(self, input: ResetAgentProfileAvatarInput) -> models.AgentProfile: "Reset an agent avatar\n\nReplaces the selected project's agent avatar with the project's default avatar, a generated planet image derived from the project ID, and returns the updated agent profile. The reset does not restore an earlier avatar: a custom avatar it replaces is discarded and must be uploaded and committed again to use it. When the default avatar is already in use, the profile is returned unchanged. This does not change the account's personal profile picture. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RESET_AGENT_PROFILE_AVATAR, payload) - return response if self._raw else response.data + return (self._raw_resource.reset_avatar(input)).data - async def update( - self, input: UpdateAgentProfileInput - ) -> models.AgentProfile | RawResponse[models.AgentProfile]: + def update(self, input: UpdateAgentProfileInput) -> models.AgentProfile: "Update an agent profile\n\nUpdates the supplied firstName and lastName fields in the project's agent profile and returns the updated profile. Avatar upload, commit and reset are separate operations. The caller must be authorized to change configuration for the selected project. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_AGENT_PROFILE, payload) - return response if self._raw else response.data + return (self._raw_resource.update(input)).data -class AsyncProjectsBillingResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw +class SyncProjectsBillingResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsBillingResource(transport) - async def get_operation( - self, input: GetBillingOperationInput - ) -> models.BillingOperation | RawResponse[models.BillingOperation]: + def get_operation(self, input: GetBillingOperationInput) -> models.BillingOperation: "Get a billing operation snapshot\n\nReturns the authoritative state of a billing operation belonging to the selected project. Use it to recover or poll a plan-change request until the operation reaches success or failure. An accepted request is not evidence that the plan change has completed." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_BILLING_OPERATION, payload) - return response if self._raw else response.data + return (self._raw_resource.get_operation(input)).data - async def get_overview( - self, input: GetBillingOverviewInput - ) -> models.GetBillingOverviewResponse | RawResponse[models.GetBillingOverviewResponse]: + def get_overview(self, input: GetBillingOverviewInput) -> models.GetBillingOverviewResponse: "Get the project's billing overview\n\nReturns the selected project's plan information, entitlements and current billing-period usage. This operation reads project billing state; it does not change plans or the payer's payment method. Organization-level plans are available through the organization billing overview." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_BILLING_OVERVIEW, payload) - return response if self._raw else response.data + return (self._raw_resource.get_overview(input)).data - async def list_billing_plans( - self, input: ListBillingPlansInput - ) -> models.ListBillingPlansResponse | RawResponse[models.ListBillingPlansResponse]: + def list_billing_plans(self, input: ListBillingPlansInput) -> models.ListBillingPlansResponse: "List available billing plans\n\nLists the billing plan catalog, grouped by their public plan-metadata type. Use the returned plan information when choosing the category and planCode for a plan change. The catalog is the same for every project, and reading it does not purchase a plan." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_BILLING_PLANS, payload) - return response if self._raw else response.data + return (self._raw_resource.list_billing_plans(input)).data -class AsyncProjectsResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw - self.agent_profile = AsyncProjectsAgentProfileResource(transport, raw) - self.billing = AsyncProjectsBillingResource(transport, raw) - self.platforms = AsyncProjectsPlatformsResource(transport, raw) +class SyncProjectsResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawProjectsResource(transport) + self.agent_profile = SyncProjectsAgentProfileResource(transport) + self.billing = SyncProjectsBillingResource(transport) + self.platforms = SyncProjectsPlatformsResource(transport) - async def create_project_api_key( + def create_project_api_key( self, input: CreateProjectApiKeyInput - ) -> models.CreateProjectApiKeyResponse | RawResponse[models.CreateProjectApiKeyResponse]: + ) -> models.CreateProjectApiKeyResponse: "Create a project API key\n\nCreates a key bound to the selected project using the supplied name, permissions and optional expiry. The secret is returned only in this response and in idempotent replays of it; store it securely because other reads never return it. The key is scoped to this project and does not grant account-level access. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return (self._raw_resource.create_project_api_key(input)).data - async def create_webhook_destination( + def create_webhook_destination( self, input: CreateWebhookDestinationInput - ) -> ( - models.CreateWebhookDestinationResponse - | RawResponse[models.CreateWebhookDestinationResponse] - ): + ) -> models.CreateWebhookDestinationResponse: "Create a webhook destination\n\nCreates a webhook destination for the selected project using its URL, payload API version, event selection and other documented settings. The response includes the signing secret, which is returned only in this response and in idempotent replays of it, never by destination reads; store it securely for signature verification. The API version must be selectable and selected event types must belong to that version's catalog. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return (self._raw_resource.create_webhook_destination(input)).data - async def delete( - self, input: DeleteProjectInput - ) -> models.Project | RawResponse[models.Project]: + def delete(self, input: DeleteProjectInput) -> models.Project: "Delete a project\n\nStarts deletion of the identified project using a credential authorized for project management. Inspect the documented response and use getProjectClosureStatus with the organization and project identifiers to read closure progress. A project API key is not an accepted credential for this operation." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DELETE_PROJECT, payload) - return response if self._raw else response.data + return (self._raw_resource.delete(input)).data - async def delete_webhook_destination( + def delete_webhook_destination( self, input: DeleteWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> models.WebhookDestination: "Delete a webhook destination\n\nDeletes the selected project's destination and returns its stable tombstone. Repeated deletion returns the deletion representation. This operation removes the destination configuration; it is separate from disabling a destination through an update." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DELETE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return (self._raw_resource.delete_webhook_destination(input)).data - async def download_attachment( - self, input: DownloadAttachmentInput - ) -> bytes | RawResponse[bytes]: + def download_attachment(self, input: DownloadAttachmentInput) -> bytes: "Download an Attachment\n\nDownloads an Attachment's bytes. If unavailable after ten seconds, returns ATTACHMENT_NOT_READY with Retry-After: 5." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DOWNLOAD_ATTACHMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.download_attachment(input)).data - async def get(self, input: GetProjectInput) -> models.Project | RawResponse[models.Project]: + def get(self, input: GetProjectInput) -> models.Project: "Get a project\n\nReturns the identified project's settings for an authorized caller. The credential must be allowed to access that project; possession of an unrelated project's key does not provide access. Missing and deleted projects are reported through the documented error responses." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_PROJECT, payload) - return response if self._raw else response.data + return (self._raw_resource.get(input)).data - async def get_attachment( - self, input: GetAttachmentInput - ) -> models.Attachment | RawResponse[models.Attachment]: + def get_attachment(self, input: GetAttachmentInput) -> models.Attachment: "Get an Attachment\n\nReturns an Attachment's metadata. Use the content endpoint to download its bytes." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_ATTACHMENT, payload) - return response if self._raw else response.data + return (self._raw_resource.get_attachment(input)).data - async def get_message_metrics_backfill( + def get_message_metrics_backfill( self, input: GetMessageMetricsBackfillInput - ) -> ( - models.GetMessageMetricsBackfillResponse - | RawResponse[models.GetMessageMetricsBackfillResponse] - ): + ) -> models.GetMessageMetricsBackfillResponse: "Get Metrics historical backfill status\n\nReturns historical metrics update progress. Completion reflects lastVerifiedAt; queries remain available during updates." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_MESSAGE_METRICS_BACKFILL, payload) - return response if self._raw else response.data + return (self._raw_resource.get_message_metrics_backfill(input)).data - async def get_message_metrics_sql_schema( + def get_message_metrics_sql_schema( self, input: GetMessageMetricsSqlSchemaInput - ) -> ( - models.GetMessageMetricsSqlSchemaResponse - | RawResponse[models.GetMessageMetricsSqlSchemaResponse] - ): + ) -> models.GetMessageMetricsSqlSchemaResponse: "Get messaging and voice metrics SQL schema\n\nReturns the message_events SQL schema, supported queries, and limits for the selected API version." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_MESSAGE_METRICS_SQL_SCHEMA, payload) - return response if self._raw else response.data + return (self._raw_resource.get_message_metrics_sql_schema(input)).data - async def get_webhook_destination( + def get_webhook_destination( self, input: GetWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> models.WebhookDestination: "Get a webhook destination\n\nReturns the configuration of one webhook destination belonging to the selected project. Missing or deleted destinations are reported as errors. This read does not disclose the signing secret returned when the destination or a secret rotation was created." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return (self._raw_resource.get_webhook_destination(input)).data - async def get_webhook_event_schema( + def get_webhook_event_schema( self, input: GetWebhookEventSchemaInput - ) -> models.WebhookEventSchema | None | RawResponse[models.WebhookEventSchema | None]: + ) -> models.WebhookEventSchema | None: "Get a webhook event schema\n\nReturns the published reader JSON Schema for eventType in the requested webhook apiVersion. Use it to interpret events for that exact payload version. The response media type is application/schema+json; an authorized conditional request may return 304 without a body. Unsupported event/version combinations are rejected." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_WEBHOOK_EVENT_SCHEMA, payload) - return response if self._raw else response.data + return (self._raw_resource.get_webhook_event_schema(input)).data - async def list_attachments( + def list_attachments(self, input: ListAttachmentsInput) -> models.AttachmentPage: + "List Project Attachments\n\nLists the Project's Attachment metadata, with optional time filters." + return (self._raw_resource.list_attachments(input)).data + + def list_project_api_keys( + self, input: ListProjectApiKeysInput + ) -> models.ListProjectApiKeysResponse: + "List project API keys\n\nLists the API keys on the selected project, ordered newest first. Revoked keys are not listed; expired keys stay listed until they are revoked. Entries contain key metadata and permissions, never secret values. Use the returned identifiers to manage an existing key; lost secrets cannot be recovered through this operation." + return (self._raw_resource.list_project_api_keys(input)).data + + def list_webhook_api_versions( + self, input: ListWebhookApiVersionsInput + ) -> models.ListWebhookApiVersionsResponse | None: + "List webhook API versions\n\nLists the published webhook payload API versions and their lifecycle metadata. The list is the same for every project. Use the selectable indicator when choosing a version for a destination. These payload dates are separate from SDK package versions. An authorized conditional request may return 304 without a response body." + return (self._raw_resource.list_webhook_api_versions(input)).data + + def list_webhook_destinations( + self, input: ListWebhookDestinationsInput + ) -> models.WebhookDestinationPage: + "List webhook destinations\n\nReturns a cursor-paginated page of active webhook destinations configured for the selected project. Use pageSize and pageToken to navigate it. The listing returns destination configuration, never signing secrets." + return (self._raw_resource.list_webhook_destinations(input)).data + + def list_webhook_egress_addresses( + self, input: ListWebhookEgressAddressesInput + ) -> models.ListWebhookEgressAddressesResponse | None: + "List webhook egress addresses\n\nReturns the public network addresses from which this environment sends webhook deliveries. Use this information when configuring the receiving system's network allowlist. The result is environment-specific and does not describe the API service's ingress addresses." + return (self._raw_resource.list_webhook_egress_addresses(input)).data + + def list_webhook_event_types( + self, input: ListWebhookEventTypesInput + ) -> models.ListWebhookEventTypesResponse | None: + "List webhook event types\n\nLists the webhook event types available in the requested apiVersion, including their descriptions, audiences and reader-schema URLs. Use this versioned catalog when selecting a destination's enabledEvents. The response may include version-retirement information; an authorized conditional request can return 304 without a body." + return (self._raw_resource.list_webhook_event_types(input)).data + + def query_message_metrics( + self, input: QueryMessageMetricsInput + ) -> models.QueryMessageMetricsResponse: + "Query messaging and voice metrics with SQL\n\nRuns read-only SQL over the Project's message_events table. Get the SQL schema for supported columns, capabilities, and limits." + return (self._raw_resource.query_message_metrics(input)).data + + def revoke_project_api_key( + self, input: RevokeProjectApiKeyInput + ) -> models.ProjectApiKeyResponse: + "Delete a project API key\n\nRevokes the identified key on the selected project and returns its revoked metadata. Repeating the deletion returns the same revokedAt value. This operation does not rotate the key or return a replacement secret. Supply the required Idempotency-Key header." + return (self._raw_resource.revoke_project_api_key(input)).data + + def rotate_webhook_signing_secret( + self, input: RotateWebhookSigningSecretInput + ) -> models.RotateWebhookSigningSecretResponse: + "Rotate a webhook signing secret\n\nRotates the signing secret for the selected project's webhook destination and returns the new secret. The optional overlapSeconds controls the requested overlap with the previous secret according to the documented request constraints. Store the new secret securely and update the receiver's signature verification configuration; it is returned only in this response and in idempotent replays of it, never by destination reads. Supply the required Idempotency-Key header." + return (self._raw_resource.rotate_webhook_signing_secret(input)).data + + def update(self, input: UpdateProjectInput) -> models.Project: + "Update a project\n\nUpdates the identified project's name and returns the updated project. The project slug is not a mutable field in this request. Use an authorized account or organization service-identity credential; a project API key is not accepted. Supply the required Idempotency-Key header." + return (self._raw_resource.update(input)).data + + def update_project_api_key( + self, input: UpdateProjectApiKeyInput + ) -> models.ProjectApiKeyResponse: + "Update a project API key's permissions\n\nReplaces the identified project key's permission list with the supplied permissions and returns the updated metadata. Sending the permission list the key already has leaves it unchanged. This request does not create a new secret or change the key's project binding. Supply the required Idempotency-Key header." + return (self._raw_resource.update_project_api_key(input)).data + + def update_webhook_destination( + self, input: UpdateWebhookDestinationInput + ) -> models.WebhookDestination: + "Update a webhook destination\n\nUpdates the supplied URL, name, description, status or enabledEvents fields on a project's webhook destination and returns its updated configuration. The payload API version is not a mutable field in this request. Event selections are checked against the destination's versioned catalog; signing-secret rotation is a separate operation. Supply the required Idempotency-Key header." + return (self._raw_resource.update_webhook_destination(input)).data + + def upload_attachment(self, input: UploadAttachmentInput) -> models.Attachment: + "Upload an Attachment\n\nUploads a file and returns its Attachment once ready to download." + return (self._raw_resource.upload_attachment(input)).data + + +class SyncAuthDeviceResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawAuthDeviceResource(transport) + + def authorize( + self, input: DeviceAuthorizeInput | None = None + ) -> models.DeviceAuthorizeResponse: + "Start Device Authorization\n\nStarts the device authorization flow for a CLI or another device without a browser. Show the verification URL and user code, then poll the token endpoint at the returned interval. No request fields are required; any supplied body is ignored." + return (self._raw_resource.authorize(input)).data + + def token(self, input: DeviceTokenInput) -> models.DeviceTokenResponse: + "Exchange Device Code or Refresh Token\n\nExchanges an authorized device code or a refresh token for an access token and rotating refresh token. Accepts JSON and form-encoded bodies. While polling, wait at least interval seconds and increase the interval on slow_down. Store the new refresh token after every successful grant.\n\nThis SDK method sends uncompressed JSON (application/json). Other request formats described above apply to direct HTTP requests." + return (self._raw_resource.token(input)).data + + +class SyncAuthResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawAuthResource(transport) + self.device = SyncAuthDeviceResource(transport) + + def begin_invitation_sso( + self, input: BeginInvitationSsoInput + ) -> models.OrganizationAuthenticationRedirect: + "Authenticate to an invitation's organization SSO connection\n\nReturns an authentication URL for the organization SSO connection associated with the supplied invitation token. Supply token and returnTo. Complete the returned authentication flow; requesting its URL does not itself accept the invitation." + return (self._raw_resource.begin_invitation_sso(input)).data + + def begin_organization_authentication( + self, input: BeginOrganizationAuthenticationInput + ) -> models.OrganizationAuthenticationRedirect: + "Authenticate to the current organization SSO connection\n\nReturns a URL to authenticate through the selected organization’s current SSO connection. Supply returnTo and open the returned URL to continue the flow. Receiving the URL does not establish an authenticated session." + return (self._raw_resource.begin_organization_authentication(input)).data + + def begin_organization_closure_authentication( + self, input: BeginOrganizationClosureAuthenticationInput + ) -> models.OrganizationAuthenticationRedirect: + "Authenticate the current Owner to inspect organization closure\n\nReturns an authentication URL for the current organization owner to inspect organization closure. Supply returnTo and complete the returned flow. This operation initiates authentication and does not close the organization." + return (self._raw_resource.begin_organization_closure_authentication(input)).data + + def begin_organization_sso_admission( + self, input: BeginOrganizationSsoAdmissionInput + ) -> models.OrganizationAuthenticationRedirect: + "Begin organization SSO admission for an existing Account\n\nReturns an SSO admission URL for an existing account and the selected organization. Supply returnTo for the continuation URL. Admission requires completing the returned authentication flow; creating the URL does not itself grant membership." + return (self._raw_resource.begin_organization_sso_admission(input)).data + + def create_organization_sso_portal_link( + self, input: CreateOrganizationSsoPortalLinkInput + ) -> models.CreateOrganizationSsoPortalLinkResponse: + "Create organization SSO setup portal\n\nReturns an organization setup portal URL. Supply returnTo and optionally intent, either sso or domain_verification; sso is the default. Open the returned URL to complete the selected setup flow." + return (self._raw_resource.create_organization_sso_portal_link(input)).data + + def disable_organization_sso( + self, input: DisableOrganizationSsoInput + ) -> models.OrganizationSsoConfiguration: + "Turn organization SSO off\n\nDeletes the provider connection, releases the SSO requirement once the connection is gone, then unbinds the chosen domains. Retry with the same Idempotency-Key to resume or await the same run." + return (self._raw_resource.disable_organization_sso(input)).data + + def get_organization_connection_status( + self, input: GetOrganizationConnectionStatusInput + ) -> models.OrganizationConnectionStatus: + "Read organization and own membership synchronization\n\nRequires current human organization membership. Synchronization status does not attest SSO configuration or completed authorization." + return (self._raw_resource.get_organization_connection_status(input)).data + + def get_organization_sso_configuration( + self, input: GetOrganizationSsoConfigurationInput + ) -> models.OrganizationSsoConfiguration: + "Read organization SSO configuration\n\nReturns the selected organization’s SSO connection state, configuration version, and desired and effective policy settings. Read policySyncStatus alongside the enforcement fields to distinguish requested settings from synchronized settings." + return (self._raw_resource.get_organization_sso_configuration(input)).data + + def list_oauth_scopes( + self, input: ListOauthScopesInput | None = None + ) -> models.ListOauthScopesResponse: + "List OAuth scopes\n\nLists the business permissions available to OAuth applications." + return (self._raw_resource.list_oauth_scopes(input)).data + + def refresh_organization_sso_connection( + self, input: RefreshOrganizationSsoConnectionInput + ) -> models.OrganizationSsoConfiguration: + "Refresh organization SSO connection\n\nRefreshes the selected organization’s SSO connection and returns its current connection state, configuration version and policy synchronization status. This operation takes no request body." + return (self._raw_resource.refresh_organization_sso_connection(input)).data + + def retry_organization_connection_sync( + self, input: RetryOrganizationConnectionSyncInput + ) -> models.OrganizationConnectionStatus: + "Retry own organization connection synchronization\n\nReconciles existing local intent. Takes no body and cannot change membership, roles or authentication policy." + return (self._raw_resource.retry_organization_connection_sync(input)).data + + def start_enterprise_login( + self, input: StartEnterpriseLoginInput + ) -> models.StartEnterpriseLoginResponse: + "Start company sign-in without an existing Account\n\nReturns a sign-in URL without requiring an existing account: the company SSO connection when the target has a ready connection, otherwise ordinary account login. Supply one documented enrollment variant: organizationId with returnTo (optionally invitationToken), invitationToken with returnTo, or retryToken. Open the returned URL to continue authentication; receiving a URL does not complete sign-in. This is a browser flow: the request must come from an allowed Origin, and the retryToken variant also needs the retry cookie set by the failed sign-in, so send it with credentials." + return (self._raw_resource.start_enterprise_login(input)).data + + def update_organization_sso_policy( + self, input: UpdateOrganizationSsoPolicyInput + ) -> models.OrganizationSsoConfiguration: + "Update organization SSO policy\n\nUpdates whether SSO can admit new members automatically using ssoJitEnabled and the current expectedVersion. Returns the organization’s SSO configuration and policy synchronization status; a successful response does not mean every desired policy setting has finished synchronizing." + return (self._raw_resource.update_organization_sso_policy(input)).data + + +class SyncOrganizationsBillingResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawOrganizationsBillingResource(transport) + + def cancel_subscription( + self, input: CancelSubscriptionInput + ) -> models.CancelSubscriptionResponse: + "Cancel a category at the end of its billing period\n\nSchedules cancellation of the specified project's billing category at the end of its current period. The category remains active through the returned cancelsAt instant and then stops renewing. This is a scheduled cancellation, not an immediate removal of the remaining period's service. If the category has no active subscription, nothing changes and the response has cancellationScheduled set to false and cancelsAt set to null. Supply both organizationId and projectId to select the project within its organization." + return (self._raw_resource.cancel_subscription(input)).data + + def change_plan( + self, input: ChangePlanInput + ) -> models.TerminalBillingOperation | models.PendingBillingOperation: + "Purchase or change a category's plan\n\nPurchases or changes the selected project's plan for the supplied category and planCode. A 202 response means the change is pending: poll the returned operation URL and honor Retry-After until it succeeds or fails. A 200 response means the idempotency key resolved to an operation that is already terminal; inspect that result rather than assuming success from the status code alone. Supply the required Idempotency-Key header. Supply both organizationId and projectId to select the project within its organization." + return (self._raw_resource.change_plan(input)).data + + def create_organization_payment_method_checkout( + self, input: CreateOrganizationPaymentMethodCheckoutInput + ) -> models.CreateOrganizationPaymentMethodCheckoutResponse: + "Get a payment-method checkout URL for the organization\n\nReturns a hosted payment-method collection URL for the selected organization. An Idempotency-Key header is optional; supply one to make retries safe. Complete the returned checkout flow. Receiving the URL does not mean a card has been saved; check payment-method status afterward." + return (self._raw_resource.create_organization_payment_method_checkout(input)).data + + def create_organization_setup_intent( + self, input: CreateOrganizationSetupIntentInput + ) -> models.CreateOrganizationSetupIntentResponse: + "Create a SetupIntent for an in-app card capture\n\nCreates payment-provider configuration for collecting a card for the selected organization and returns clientSecret and publishableKey. Supply the required Idempotency-Key header. Complete the provider’s card-collection flow separately and avoid logging the returned client secret." + return (self._raw_resource.create_organization_setup_intent(input)).data + + def get_organization_billing_overview( + self, input: GetOrganizationBillingOverviewInput + ) -> models.GetOrganizationBillingOverviewResponse: + "Get the organization's billing overview\n\nReturns the selected organization’s billing subscription and entitlementsVersion. The subscription can be null. Read the returned plan, charges and entitlements to inspect organization billing; this operation does not purchase or change a plan." + return (self._raw_resource.get_organization_billing_overview(input)).data + + def get_organization_payment_method( + self, input: GetOrganizationPaymentMethodInput + ) -> models.GetOrganizationPaymentMethodResponse: + "Check the organization for a card on file\n\nReports whether the selected organization has a card on file and returns its documented payment-method metadata. Reading this endpoint does not collect a new card or create a checkout session." + return (self._raw_resource.get_organization_payment_method(input)).data + + def list_invoices(self, input: ListInvoicesInput) -> models.ListInvoicesResponse: + "List invoices\n\nReturns a single page of the selected organization's invoices; invoices with a zero total are excluded. Use the documented invoice fields to inspect each invoice's billing state. Listing invoices does not make a payment or modify a subscription." + return (self._raw_resource.list_invoices(input)).data + + def resume_subscription( + self, input: ResumeSubscriptionInput + ) -> models.ResumeSubscriptionResponse: + "Resume a category scheduled for cancellation\n\nRemoves a scheduled cancellation for the specified billing category on the selected project so it can renew normally. This operation resumes a category scheduled to cancel; it is separate from purchasing or changing a plan. Supply both organizationId and projectId to select the project within its organization." + return (self._raw_resource.resume_subscription(input)).data + + +class SyncOrganizationsProjectsResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawOrganizationsProjectsResource(transport) + + def check_project_slug_availability( + self, input: CheckProjectSlugAvailabilityInput + ) -> models.CheckProjectSlugAvailabilityResponse: + "Check slug availability\n\nReports whether createProject would accept `slug` right now. Advisory: only the create itself allocates, so a caller must still handle SLUG_TAKEN. A malformed slug is rejected on shape; a reserved slug, a slug held by an active project, and a slug retired with a deleted project each answer `available: false` with a reason." + return (self._raw_resource.check_project_slug_availability(input)).data + + def count(self, input: CountProjectsInput) -> models.ProjectCount: + "Count accessible projects\n\nCounts the projects the same filter would list. The count is read from the primary, so it is authoritative rather than replica-lagged." + return (self._raw_resource.count(input)).data + + def create(self, input: CreateProjectInput) -> models.Project: + "Create a project\n\nCreates in the authorized organization. In addition to account credentials, explicitly granted Service Identity API keys and M2M tokens may create projects. Project API keys cannot create projects. Creator and private credential evidence come only from the trusted authorization context. The caller-selected slug is immutable, must be 3 to 63 lowercase ASCII alphanumerics separated by single hyphens, and cannot be reserved or held by any active or deleted project." + return (self._raw_resource.create(input)).data + + def get_project_closure_status( + self, input: GetProjectClosureStatusInput + ) -> models.GetProjectClosureStatusResponse: + "Read project closure progress\n\nReturns closure progress for projectId within organizationId, including deletionOperationId, domain progress and ready. This read operation does not initiate deletion." + return (self._raw_resource.get_project_closure_status(input)).data + + def list(self, input: ListProjectsInput) -> models.ProjectPage: + "List accessible projects\n\nReturns a cursor-paginated page of the active projects in organizationId. Filter using query and the documented creation-time bounds, and navigate with pageSize and pageToken. Project roles are not returned and role is not a supported filter." + return (self._raw_resource.list(input)).data + + +class SyncOrganizationsResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawOrganizationsResource(transport) + self.billing = SyncOrganizationsBillingResource(transport) + self.projects = SyncOrganizationsProjectsResource(transport) + + +class SyncAccountResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawAccountResource(transport) + + def commit_profile_picture(self, input: CommitAccountProfilePictureInput) -> models.Account: + "Commit a profile picture\n\nCommits a profile picture previously uploaded through createAccountProfilePictureUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's ownership, size, content type, image bytes, dimensions, encryption, and age before changing the Account." + return (self._raw_resource.commit_profile_picture(input)).data + + def confirm_phone_verification( + self, input: ConfirmAccountPhoneVerificationInput + ) -> models.Account: + "Confirm a phone number verification\n\nBinds the number once the code is approved. Repeat calls return the bound Account." + return (self._raw_resource.confirm_phone_verification(input)).data + + def create_account_service_key( + self, input: CreateAccountServiceKeyInput + ) -> models.CreateAccountServiceKeyResponse: + "Create an Account Service Key\n\nCreates a service key for the authenticated account with the supplied name and optional expiresAt. Returns key metadata and a one-time credential; store the credential securely because it cannot be retrieved through the listing endpoint. These credentials act as the account and must not be distributed as project-scoped keys. Supply the required Idempotency-Key header." + return (self._raw_resource.create_account_service_key(input)).data + + def create_profile_picture_upload( + self, input: CreateAccountProfilePictureUploadInput + ) -> models.ProfilePictureUpload: + "Create a profile picture upload\n\nCreates a ten-minute, Account-bound presigned S3 POST for a JPEG, PNG, or WebP profile picture up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAccountProfilePicture with the returned uploadId. Do not cache or log the upload URL or form fields." + return (self._raw_resource.create_profile_picture_upload(input)).data + + def delete(self, input: DeleteAccountInput | None = None) -> models.Account: + "Delete the authenticated account\n\nDeletes the authenticated account and returns its account tombstone. The operation is rejected while the account still owns organizations; transfer or close those organizations before retrying. This endpoint acts on the caller's account and does not accept another account's identifier." + return (self._raw_resource.delete(input)).data + + def get(self, input: GetAccountInput | None = None) -> models.Account: + "Get the authenticated account\n\nReturns the profile of the authenticated account. The account is selected from the credential rather than a request parameter. A missing or deleted account is reported as an error instead of an empty profile." + return (self._raw_resource.get(input)).data + + def list_account_service_keys( + self, input: ListAccountServiceKeysInput | None = None + ) -> models.ListAccountServiceKeysResponse: + "List Account Service Keys\n\nReturns metadata for the authenticated account's unrevoked service keys, including expired keys, ordered newest first. Secret values are not returned; a key's credential is disclosed only when that key is created." + return (self._raw_resource.list_account_service_keys(input)).data + + def list_authorized_applications( + self, input: ListAuthorizedApplicationsInput | None = None + ) -> models.ListAuthorizedApplicationsResponse: + "List connected applications\n\nLists the OAuth applications authorized by the authenticated user." + return (self._raw_resource.list_authorized_applications(input)).data + + def reset_profile_picture(self, input: ResetAccountProfilePictureInput) -> models.Account: + "Remove a profile picture\n\nRemoves the authenticated account's custom profile picture and returns the account using its default picture. This operation does not upload a replacement; use the upload-and-commit operations when setting a new custom picture. Supply the required Idempotency-Key header." + return (self._raw_resource.reset_profile_picture(input)).data + + def revoke_account_service_key( + self, input: RevokeAccountServiceKeyInput + ) -> models.RevokeAccountServiceKeyResponse: + "Revoke an Account Service Key\n\nRevokes the account-owned service key identified by serviceKeyId and returns its revoked metadata. Repeating the revocation is stable. Revocation changes the credential's validity; it does not create a replacement key. Supply the required Idempotency-Key header." + return (self._raw_resource.revoke_account_service_key(input)).data + + def revoke_authorized_application(self, input: RevokeAuthorizedApplicationInput) -> None: + "Revoke a connected application\n\nRevokes the authenticated user's grant for one OAuth application." + return (self._raw_resource.revoke_authorized_application(input)).data + + def start_phone_verification( + self, input: StartAccountPhoneVerificationInput + ) -> models.StartAccountPhoneVerificationResponse: + "Start a phone number verification\n\nSends an SMS code. Answers CAPTCHA_REQUIRED with the widget to render when no solved challenge accompanies the request; retry with the returned challengeContext and a token. Rate limited per account, per destination number, and globally; a rejection carries Retry-After." + return (self._raw_resource.start_phone_verification(input)).data + + def update(self, input: UpdateAccountInput) -> models.Account: + "Update the authenticated account\n\nUpdates the supplied firstName and lastName fields on the authenticated account and returns the updated profile. Only the documented profile fields can be changed through this endpoint; profile-picture uploads and phone-number verification use their dedicated operations. Supply the required Idempotency-Key header." + return (self._raw_resource.update(input)).data + + +class SyncSystemResource: + def __init__(self, transport: SyncTransport) -> None: + self._raw_resource = SyncRawSystemResource(transport) + + def create_app_installation_request( + self, input: CreateAppInstallationRequestInput + ) -> models.CreateAppInstallationRequestResponse: + "Request an app installation\n\nAuthenticates a registered app backend using a short-lived signed client assertion. Creates request metadata only; customer approval is still required." + return (self._raw_resource.create_app_installation_request(input)).data + + def redeem_app_installation_delivery( + self, input: RedeemAppInstallationDeliveryInput + ) -> models.RedeemAppInstallationDeliveryResponse: + "Redeem an approved installation credential\n\nThe registered app backend authenticates with a signed client assertion and a single-use code. Plaintext is returned only once; retries return status and never create another credential." + return (self._raw_resource.redeem_app_installation_delivery(input)).data + + +class AsyncRawProjectsPlatformsImessageAssignmentsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def create( + self, input: CreateSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Create shared line assignment\n\nMaps an end user's iMessage handle — an E.164 phone number or an email address — onto one of the project's pooled shared iMessage lines, consuming a seat from the project's entitlement. The assigned number is allocated by the server. When an email address is supplied in `email` the user is sent an invite asynchronously to that address; it is never inferred from the handle, and the response never reports whether the send succeeded. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_SHARED_LINE_ASSIGNMENT, payload) + + async def get( + self, input: GetSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Get shared line assignment\n\nReads one shared line assignment. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_SHARED_LINE_ASSIGNMENT, payload) + + async def list( + self, input: ListSharedLineAssignmentsInput + ) -> RawResponse[models.SharedLineAssignmentPage]: + "List shared line assignments\n\nLists the project's shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_SHARED_LINE_ASSIGNMENTS, payload) + + async def release( + self, input: ReleaseSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Release shared line assignment\n\nReleases a shared line assignment, freeing both its seat and its handle for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RELEASE_SHARED_LINE_ASSIGNMENT, payload) + + +class AsyncRawProjectsPlatformsImessageResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + self.assignments = AsyncRawProjectsPlatformsImessageAssignmentsResource(transport) + + +class AsyncRawProjectsPlatformsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + self.imessage = AsyncRawProjectsPlatformsImessageResource(transport) + + async def assign_sms_line_campaign( + self, input: AssignSmsLineCampaignInput + ) -> RawResponse[models.Operation]: + "Assign or replace SMS line campaign\n\nAttach a ready campaign from this project’s organization to its line. Requires platforms:write for the project; human and machine actors retain their authenticated identity. Requires a permanent Idempotency-Key and the current assignment version. Provider provisioning runs asynchronously." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_ASSIGN_SMS_LINE_CAMPAIGN, payload) + + async def assign_voice_line_profile( + self, input: AssignVoiceLineProfileInput + ) -> RawResponse[models.VoiceLineProfileAssignment]: + "Assign Voice line profile\n\nAssigns or replaces a line's explicit additional-profile override when the resource version matches. The current default cannot be assigned explicitly. The pstn_voice ability remains the admission source of truth. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_ASSIGN_VOICE_LINE_PROFILE, payload) + + async def batch_update_voice_line_profile_assignments( + self, input: BatchUpdateVoiceLineProfileAssignmentsInput + ) -> RawResponse[models.BatchUpdateVoiceLineProfileAssignmentsResponse]: + "Batch update Voice line profile assignments\n\nAtomically sets additional-profile overrides or switches lines back to the project default for up to 100 Voice-capable lines. A null profileId means use the default. Every expected resource version must match or no line changes. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request( + _OP_BATCH_UPDATE_VOICE_LINE_PROFILE_ASSIGNMENTS, payload + ) + + async def cancel_operation(self, input: CancelOperationInput) -> RawResponse[models.Operation]: + "Cancel operation\n\nWithdraws a provision that has not been fulfilled yet. This is an operations action rather than a DELETE, because there is nothing to delete: no resource exists until the work commits. Whether it is accepted depends on the resource type — a dedicated iMessage line may sit waiting on inventory for hours and withdrawing costs nothing, while an SMS number is cancellable during inventory waiting and answers 409 once the workflow commits to its first provider order. Campaign assignment and detachment operations cannot be cancelled in any state. Wait for completion before requesting another change; that new change is not a guaranteed rollback. The output-only `cancellable` field is a snapshot; the cancellation transaction always checks the current phase under a row lock. A cancel that loses the race against the work finishing also answers 409: the resource exists and is billed for, so what you want then is to release it. Nothing is charged for a cancelled provision — billing runs after the work, so there is never anything to refund. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CANCEL_OPERATION, payload) + + async def configure_voice_profile_outbound( + self, input: ConfigureVoiceProfileOutboundInput + ) -> RawResponse[models.ConfigureVoiceProfileOutboundResponse]: + "Configure Voice profile outbound credential\n\nConfigures a SIP credential for outbound calls from a profile when the shared profile version matches. authentication.algorithm is required: SHA-256 is recommended, while MD5 is a weaker legacy option supported over UDP, TCP, and TLS; TLS is strongly recommended because UDP and TCP do not encrypt SIP signaling. The profileId may identify the default or an additional profile. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CONFIGURE_VOICE_PROFILE_OUTBOUND, payload) + + async def connect_email_domain( + self, input: ConnectEmailDomainInput + ) -> RawResponse[models.Operation]: + "Connect email domain\n\nReserves a normalized DNS domain and starts its durable email-provider setup. The accepted provision consumes one email-domain entitlement slot until it fails, is cancelled, or becomes a live resource; the plan's email.max_email_domains value sets the project limit. The customer resource does not exist until provider identity and DNS setup reach READY; poll the returned operation for progress. A domain may have only one unfinished provision or live resource globally. Email domains have no additional per-domain charge. The Idempotency-Key is required and permanent: replaying the same key and canonical domain returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CONNECT_EMAIL_DOMAIN, payload) + + async def connect_telegram_bot( + self, input: ConnectTelegramBotInput + ) -> RawResponse[models.Operation]: + "Connect Telegram bot\n\nStarts a free managed Telegram bot connection. Each project may have one unfinished Telegram provision, including user interaction and failure cleanup. A different Idempotency-Key while one is active returns 409 TELEGRAM_PROVISION_IN_PROGRESS with its operationId and operationUrl; resume it, cancel it while cancellation is available, or wait for it to finish. Rejected keys remain reusable. Open detail.setupUrl to connect an existing managed bot or create a new one with the project's default agent name or a custom display name, then poll Location. The link remains usable while the operation is active and never expires. Replaying the same Idempotency-Key returns the original operation, even after completion or while a newer setup is active. POST, GET and list share the same operation details. The API includes detail.setupUrl only for callers with platforms:write for the project; read-only callers receive the other details unchanged." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CONNECT_TELEGRAM_BOT, payload) + + async def connect_whatsapp_business( + self, input: ConnectWhatsappBusinessInput + ) -> RawResponse[models.Operation]: + "Connect WhatsApp Business\n\nExchanges the authorization code Embedded Signup returned and connects exactly the selected phone number as one `whatsapp_sender`. Send the WABA id and phone-number id emitted by the same popup attempt; both are treated as selectors and verified against Meta before use. A selected number that matches a non-retired, same-project `voip_line` is linked to it; a number absent from Photon inventory stays unbound; a matching non-retired `cosmos_line`, foreign VoIP line or unassigned VoIP line fails the operation before registration. Connecting is free — no plan requirement — but Billing must report the project's organization as ready with a payment method on file. The Idempotency-Key is required and permanent: replaying the same key returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CONNECT_WHATSAPP_BUSINESS, payload) + + async def create_default_voice_profile( + self, input: CreateDefaultVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Create default Voice profile\n\nCreates the project default Voice profile when absent. An identical replay returns the existing default without changing its version; a different existing default conflicts. Direction configuration is managed separately. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_DEFAULT_VOICE_PROFILE, payload) + + async def create_voice_profile( + self, input: CreateVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Create Voice profile\n\nCreates a direction-neutral additional Voice profile. The project default must already exist. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_VOICE_PROFILE, payload) + + async def create_whatsapp_shared_line_assignment( + self, input: CreateWhatsappSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Create WhatsApp shared line assignment\n\nMaps an end user's phone number onto one of the project's pooled shared WhatsApp lines, consuming a seat from the project's WhatsApp entitlement. The assigned number is allocated by the server. When an email address is supplied the user is sent an invite asynchronously; the response never reports whether that succeeded. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) + + async def create_whatsapp_voip_sender( + self, input: CreateWhatsappVoipSenderInput + ) -> RawResponse[models.Operation]: + "Create a VoIP-backed WhatsApp sender\n\nRegisters an active, SMS-capable Photon VoIP line on this project's connected WhatsApp Business Account. The account is resolved server-side; callers never select a WABA. The platform creates or reuses the Meta number, requests and consumes the SMS ownership code internally, verifies it, and registers the sender. displayName is optional; when omitted the project agent profile name is snapshotted before acceptance. The VoIP line remains a separate resource and never receives the whatsapp_business ability." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_WHATSAPP_VOIP_SENDER, payload) + + async def delete_voice_profile(self, input: DeleteVoiceProfileInput) -> RawResponse[None]: + "Delete Voice profile\n\nDeletes an additional profile when expectedVersion matches. Assigned profiles require force=true, which atomically removes every stored override so affected lines follow the default. The default can never be deleted. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DELETE_VOICE_PROFILE, payload) + + async def delete_voice_profile_inbound( + self, input: DeleteVoiceProfileInboundInput + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: + "Remove Voice profile inbound configuration\n\nRemoves a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. The profile and its line assignments remain. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DELETE_VOICE_PROFILE_INBOUND, payload) + + async def delete_voice_profile_outbound( + self, input: DeleteVoiceProfileOutboundInput + ) -> RawResponse[models.DeleteVoiceProfileOutboundResponse]: + "Revoke Voice profile outbound credential\n\nRevokes outbound calling for a profile when the shared profile version matches. The profileId may identify the default or an additional profile. The profile, inbound destination, and line assignments remain. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DELETE_VOICE_PROFILE_OUTBOUND, payload) + + async def disconnect_whatsapp_business_account( + self, input: DisconnectWhatsappBusinessAccountInput + ) -> RawResponse[models.Operation]: + "Disconnect WhatsApp Business account and numbers\n\nDisconnects every attached WhatsApp sender, then unsubscribes our app and removes the project's business account connection. Photon VoIP lines and the numbers in Meta remain. Requires Idempotency-Key. Poll the returned operation; provider refusals appear as operation failures and retain the account for retry with a new key. New signups are blocked while disconnecting, and existing provisions must finish before this request can be accepted." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DISCONNECT_WHATSAPP_BUSINESS_ACCOUNT, payload) + + async def get_default_voice_profile( + self, input: GetDefaultVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Get default Voice profile\n\nGets the profile currently selected as the project default, including its optional inbound delivery state. Requires platforms:read bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_DEFAULT_VOICE_PROFILE, payload) + + async def get_imessage( + self, input: GetProjectImessagePlatformInput + ) -> RawResponse[models.ProjectPlatformSettings]: + "Get project iMessage platform\n\nReports whether the project is on shared or dedicated iMessage lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_PROJECT_IMESSAGE_PLATFORM, payload) + + async def get_operation( + self, input: GetOperationInput + ) -> RawResponse[models.GetOperationResponse]: + "Get operation\n\nReads one operation using the same operation representation as creation and list. The API includes detail.setupUrl only with platforms:write for this project. This is the polling endpoint every asynchronous request here points its Location at, and it resolves from the moment that request is accepted — an operation is committed before its work is dispatched, so there is no window in which the URL 404s. Poll until `state` is one of `succeeded`, `failed` or `cancelled`, pacing from the Retry-After the accepting response returned. While an email domain waits for DNS, `detail` always contains the manual records and may additionally contain `automaticSetup` with a signed provider URL to open separately. Once the operation has produced a resource, the response carries that resource too, so the poll that finishes is also the one that tells you what you got. `succeeded` means the work is done; billing runs behind it and is not something the caller waits on. Operations are never purged, so a 404 means the id was never this project's. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_OPERATION, payload) + + async def get_project_whatsapp_platform( + self, input: GetProjectWhatsappPlatformInput + ) -> RawResponse[models.ProjectPlatformSettings]: + "Get project WhatsApp platform\n\nReports whether the project is on shared or dedicated WhatsApp lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_PROJECT_WHATSAPP_PLATFORM, payload) + + async def get_resource(self, input: GetResourceInput) -> RawResponse[models.Resource]: + "Get resource\n\nReads one resource the project holds. A released number stays readable and reads `retired`, because it remains part of this project's history. A dedicated line given back does NOT: returning it to inventory is what makes it claimable by someone else, so it answers 404 and the operation that returned it is the record that this project once held it. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_RESOURCE, payload) + + async def get_sms_line_campaign_assignment( + self, input: GetSmsLineCampaignAssignmentInput + ) -> RawResponse[models.GetSmsLineCampaignAssignmentResponse]: + "Read SMS line campaign assignment\n\nRead the last confirmed campaign and current eligibility. Follow changes through their operations. Eligibility is a control-plane assessment, not a delivery or recipient-consent guarantee." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_SMS_LINE_CAMPAIGN_ASSIGNMENT, payload) + + async def get_voice_line_profile_assignment( + self, input: GetVoiceLineProfileAssignmentInput + ) -> RawResponse[models.VoiceLineProfileAssignment]: + "Get Voice line profile assignment\n\nGets the explicit additional-profile override for an owned Voice-capable line. A line following the project default returns 200 without profileId. Requires platforms:read bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_VOICE_LINE_PROFILE_ASSIGNMENT, payload) + + async def get_voice_profile( + self, input: GetVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Get Voice profile\n\nGets one reusable Voice profile, including its optional inbound delivery state. Requires platforms:read bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_VOICE_PROFILE, payload) + + async def get_whatsapp_business_account( + self, input: GetWhatsappBusinessAccountInput + ) -> RawResponse[models.WhatsappBusinessAccount]: + "Get WhatsApp Business account\n\nGets the one WhatsApp Business Account this project has connected, with its number of live senders. Senders are resources and are listed by GET /platforms/resources?ability=whatsapp_business. The account is not a resource and carries no access token. Meta's retained numbers are listed separately by GET /platforms/whatsapp-business/account/phone-numbers. `subscribedAt` is absent until our app is attached to the account's webhooks. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WHATSAPP_BUSINESS_ACCOUNT, payload) + + async def get_whatsapp_business_verification_code( + self, input: GetWhatsappBusinessVerificationCodeInput + ) -> RawResponse[models.GetWhatsappBusinessVerificationCodeResponse]: + "Get WhatsApp Business verification code\n\nReturns the latest six-digit WhatsApp Business ownership code received by SMS for an active Photon VOIP number, but only when its provider timestamp is strictly newer than the required receivedAfter boundary. receivedAfter must be an RFC 3339 timestamp between this request's arrival time and two minutes before it; once it expires, restart Meta's verification flow with a new boundary. A missing newer code is a retryable 404 with Retry-After: 2. Poll after 2, 4, 8, then 10 seconds, applying ±20% jitter and capping later intervals at 10 seconds. Stop when the original boundary is two minutes old. Responses are never cached. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WHATSAPP_BUSINESS_VERIFICATION_CODE, payload) + + async def get_whatsapp_shared_line_assignment( + self, input: GetWhatsappSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Get WhatsApp shared line assignment\n\nReads one WhatsApp shared line assignment. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) + + async def get_whatsapp_signup_config( + self, input: GetWhatsappSignupConfigInput + ) -> RawResponse[models.GetWhatsappSignupConfigResponse]: + "Get WhatsApp signup config\n\nReturns what the browser needs to open Meta's Embedded Signup popup: the Facebook Login for Business configuration id, the Graph version to run against, and the scopes it will request. Answered in-process rather than forwarded, so the first step of onboarding survives an outage of the private service. Pass `configId` to `FB.login` as `config_id` with `response_type: 'code'` and `override_default_response_type: true`. Do NOT add a `featureType` — omitting it is what keeps the phone-number screen in the flow, and `only_waba_sharing` produces an account with no number that cannot be provisioned. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WHATSAPP_SIGNUP_CONFIG, payload) + + async def list_number_area_codes( + self, input: ListNumberAreaCodesInput + ) -> RawResponse[models.ListNumberAreaCodesResponse]: + "List supported number area codes\n\nLists current provider coverage for US local numbers, sorted and deduplicated. Coverage does not guarantee inventory carrying every required feature. New area-specific purchases must use a listed code; accepted purchases keep waiting if coverage later changes. Requires platforms:read for the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_NUMBER_AREA_CODES, payload) + + async def list_number_countries( + self, input: ListNumberCountriesInput + ) -> RawResponse[models.ListNumberCountriesResponse]: + "List supported number countries\n\nLists supported purchase countries independently of current provider inventory. Requires platforms:read for the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_NUMBER_COUNTRIES, payload) + + async def list_operations( + self, input: ListOperationsInput + ) -> RawResponse[models.OperationPage]: + "List operations\n\nLists the project's operations using the same operation representation as creation and GET. The API includes detail.setupUrl only with platforms:write for this project. Results are oldest first — every provision and release it has ever asked for, including the ones still running. This is the entire in-flight view: a resource only appears once it is real, so nothing half-built shows up in the resource list and nothing in flight is missing from this one. Filter by `resourceId` to get one resource's whole history, which for a pooled line is every tenure this project has had on it. `state` is comma-separated; `type` accepts one operation type and an absent filter means everything, including failed and cancelled operations. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_OPERATIONS, payload) + + async def list_project_platforms( + self, input: ListProjectPlatformsInput + ) -> RawResponse[models.ListProjectPlatformsResponse]: + "List project platforms\n\nLists the platform types available to this project. Every project currently sees the same fixed public contract, answered in-process rather than forwarded, so the list survives an outage of the private service. The project binding exists so that answer can narrow per project without moving the route. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_PROJECT_PLATFORMS, payload) + + async def list_resources(self, input: ListResourcesInput) -> RawResponse[models.ResourcePage]: + "List resources\n\nLists everything the project holds, oldest first, whatever kind of thing it is — one endpoint and one id shape for numbers, dedicated lines and whatever ships next. Nothing half-built appears here: a resource exists only once it is real, so anything still being provisioned is an operation rather than a resource with a pending flag. Filter by `type`, by `ability` (which matches only abilities that are currently enabled), and by `state` — comma-separated, and absent means every state, including retired ones. `detail` carries a per-type public view: an SMS number's number, a dedicated line's number and whether it is healthy. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_RESOURCES, payload) + + async def list_voice_profiles( + self, input: ListVoiceProfilesInput + ) -> RawResponse[models.VoiceProfilePage]: + "List Voice profiles\n\nLists reusable Voice profiles in this project. Requires platforms:read bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_VOICE_PROFILES, payload) + + async def list_whatsapp_account_phone_numbers( + self, input: ListWhatsappAccountPhoneNumbersInput + ) -> RawResponse[models.ListWhatsappAccountPhoneNumbersResponse]: + "List WhatsApp account phone numbers\n\nLists the connected WABA's phone numbers directly from Meta, including numbers whose Photon sender was disconnected. Ownership is photon for a number in this project's current Photon inventory and meta otherwise. Match a Photon SMS number by its E.164 phoneNumber and reuse its existing displayName when reconnecting. A null name is unavailable, not permission to choose a new name. A failed lookup returns an error rather than an empty list. Requires platforms:read on the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WHATSAPP_ACCOUNT_PHONE_NUMBERS, payload) + + async def list_whatsapp_shared_line_assignments( + self, input: ListWhatsappSharedLineAssignmentsInput + ) -> RawResponse[models.SharedLineAssignmentPage]: + "List WhatsApp shared line assignments\n\nLists the project's WhatsApp shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WHATSAPP_SHARED_LINE_ASSIGNMENTS, payload) + + async def provision_imessage_dedicated_line( + self, input: ProvisionImessageDedicatedLineInput + ) -> RawResponse[models.Operation]: + "Provision dedicated iMessage line\n\nClaims one dedicated iMessage line for the project and enables iMessage on it. Always answers 202 with an operation: dedicated lines are allocated from available capacity, and unavailable capacity causes a wait rather than a failure — this can legitimately stay `running` for hours, which is exactly why the response is a handle to poll rather than a number. The project's messaging subscription must grant the dedicated iMessage lines entitlement (`imessage_dedicated_lines.can_purchase`), and that is checked before capacity is reserved; nothing is charged until a line is actually claimed. If you no longer want to wait, POST to the operation's cancel endpoint, which costs nothing. The Idempotency-Key is required and permanent: repeating it returns the same operation forever. A further line always needs a NEW key, including while others are still waiting. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_PROVISION_IMESSAGE_DEDICATED_LINE, payload) + + async def provision_whatsapp_dedicated_line( + self, input: ProvisionWhatsappDedicatedLineInput + ) -> RawResponse[models.Operation]: + "Provision dedicated WhatsApp line\n\nProvisions one dedicated WhatsApp line with WhatsApp and shared Voice enabled. It attaches to an eligible iMessage line the project already owns when possible so both products keep the same number; otherwise it claims healthy, available WhatsApp-capable dedicated-line inventory. Always answers 202, because waiting when no inventory is available is not a failure. The product opens its own charge period after the abilities are enabled; Voice has no separate charge. Cancel the returned operation to stop waiting. The Idempotency-Key is required and permanent." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_PROVISION_WHATSAPP_DEDICATED_LINE, payload) + + async def purchase_sms_number( + self, input: PurchaseSmsNumberInput + ) -> RawResponse[models.Operation]: + "Purchase SMS number\n\nBuys one US local number from the provider and records it as a resource with SMS enabled. Requires countryCode (US) and accepts an optional three-digit geographic areaCode. The server selects an exact matching number. Empty inventory keeps the operation running until a number is available or the caller cancels before ordering begins. Always answers 202 with an operation: the work runs behind the response, and the Location points at the operation to poll. New area-specific requests must appear in current provider coverage; discover it with GET /sms/numbers/area-codes?countryCode=US. Coverage and subscription checks run before operation creation. Billing follows delivery. Replays return the original operation without checking current coverage. The Idempotency-Key is required and permanent: repeating it returns the same operation forever, never a second number. A further number always needs a NEW key, including while others are still running. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_PURCHASE_SMS_NUMBER, payload) + + async def release_imessage_dedicated_line( + self, input: ReleaseImessageDedicatedLineInput + ) -> RawResponse[models.Operation]: + "Release dedicated iMessage line\n\nRemoves only iMessage from one dedicated line. Shared Voice is removed only when WhatsApp is absent; if WhatsApp remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open iMessage charge period identifies this product tenure." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RELEASE_IMESSAGE_DEDICATED_LINE, payload) + + async def release_resource(self, input: ReleaseResourceInput) -> RawResponse[models.Operation]: + "Release resource\n\nGives one resource back, whatever it is. What that means is the resource's own business: an SMS number goes back to the provider and is retired, a dedicated iMessage line goes back to the shared pool and stays in existence for someone else to claim. Either way the provider is contacted first where there is one, then a single transaction disables every ability, ends the project's hold and closes the charge period — so a provider that refuses leaves the resource exactly as it was, still owned and still billed. Usually finishes inside this request and answers 200; if the provider is slow it answers 202 and the Location points at the operation to poll. The decrement runs behind the answer either way, so the resource is gone when you are told it is. Takes no Idempotency-Key — releasing the same resource twice is the same request. Releasing one that is already gone answers 404. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RELEASE_RESOURCE, payload) + + async def release_whatsapp_dedicated_line( + self, input: ReleaseWhatsappDedicatedLineInput + ) -> RawResponse[models.Operation]: + "Release dedicated WhatsApp line\n\nRemoves only WhatsApp from one dedicated line. Shared Voice is removed only when iMessage is absent; if iMessage remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open WhatsApp charge period identifies this product tenure." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RELEASE_WHATSAPP_DEDICATED_LINE, payload) + + async def release_whatsapp_shared_line_assignment( + self, input: ReleaseWhatsappSharedLineAssignmentInput + ) -> RawResponse[models.SharedLineAssignment]: + "Release WhatsApp shared line assignment\n\nReleases a WhatsApp shared line assignment, freeing its seat for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RELEASE_WHATSAPP_SHARED_LINE_ASSIGNMENT, payload) + + async def replace_voice_profile_inbound( + self, input: ReplaceVoiceProfileInboundInput + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: + "Create or replace Voice profile inbound configuration\n\nCreates or fully replaces a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. Credentials are required and nullable; null removes destination authentication. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REPLACE_VOICE_PROFILE_INBOUND, payload) + + async def rotate_voice_profile_outbound_credential( + self, input: RotateVoiceProfileOutboundCredentialInput + ) -> RawResponse[models.RotateVoiceProfileOutboundCredentialResponse]: + "Rotate Voice outbound credential\n\nRotates a SIP profile's outbound credential when expectedVersion matches. The profileId may identify the default or an additional profile. Normal rotation gives the previous credential one hour of grace; emergency rotation gives none. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_ROTATE_VOICE_PROFILE_OUTBOUND_CREDENTIAL, payload) + + async def unassign_sms_line_campaign( + self, input: UnassignSmsLineCampaignInput + ) -> RawResponse[models.Operation]: + "Remove SMS line campaign\n\nAny project writer, including a scoped API key, may detach the campaign. The number and campaign remain owned. Requires a permanent Idempotency-Key and expectedVersion. Local eligibility is blocked immediately; provider detachment runs asynchronously." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UNASSIGN_SMS_LINE_CAMPAIGN, payload) + + async def unassign_voice_line_profile( + self, input: UnassignVoiceLineProfileInput + ) -> RawResponse[None]: + "Unassign Voice line profile\n\nRemoves a line's explicit override when the resource version matches so the line follows the project default. Profiles and the pstn_voice ability are unchanged. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UNASSIGN_VOICE_LINE_PROFILE, payload) + + async def update_default_voice_profile( + self, input: UpdateDefaultVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Update default Voice profile\n\nPatches the default profile's protocol or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Its server-assigned name is immutable, and directional configuration uses the profileId returned by this resource. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_DEFAULT_VOICE_PROFILE, payload) + + async def update_voice_profile( + self, input: UpdateVoiceProfileInput + ) -> RawResponse[models.VoiceProfile]: + "Update Voice profile\n\nPatches an additional profile's name, protocol, or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Directional configuration is managed through the profile's inbound and outbound endpoints. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_VOICE_PROFILE, payload) + + async def update_voice_profile_inbound( + self, input: UpdateVoiceProfileInboundInput + ) -> RawResponse[models.VoiceProfileInboundConfiguration]: + "Update Voice profile inbound configuration\n\nUpdates selected fields of a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. At least one of destinationUri or credentials is required. Credential omission preserves destination authentication, null removes it, and an object replaces it atomically. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_VOICE_PROFILE_INBOUND, payload) + + async def update_voice_profile_outbound_authentication( + self, input: UpdateVoiceProfileOutboundAuthenticationInput + ) -> RawResponse[models.UpdateVoiceProfileOutboundAuthenticationResponse]: + "Update Voice profile outbound authentication policy\n\nChanges a SIP profile's outbound Digest algorithm when expectedVersion matches. The profileId may identify the default or an additional profile. This policy-only change preserves the password, username, and any previous-password grace deadline. SHA-256 is recommended; MD5 is a weaker legacy option. Returns non-secret outbound metadata and the profile version. Requires platforms:write bound to the path project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request( + _OP_UPDATE_VOICE_PROFILE_OUTBOUND_AUTHENTICATION, payload + ) + + +class AsyncRawProjectsAgentProfileResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def commit_avatar( + self, input: CommitAgentProfileAvatarInput + ) -> RawResponse[models.AgentProfile]: + "Commit an agent avatar\n\nCommits an agent avatar previously uploaded through createAgentProfileAvatarUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's Project ownership, size, content type, image bytes, dimensions, encryption, and age before changing the agent profile." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_COMMIT_AGENT_PROFILE_AVATAR, payload) + + async def create_avatar_upload( + self, input: CreateAgentProfileAvatarUploadInput + ) -> RawResponse[models.AgentProfileAvatarUpload]: + "Create an agent avatar upload\n\nCreates a ten-minute, Project-bound presigned S3 POST for a JPEG, PNG, or WebP agent avatar up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAgentProfileAvatar with the returned uploadId. Do not cache or log the upload URL or form fields." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_AGENT_PROFILE_AVATAR_UPLOAD, payload) + + async def get(self, input: GetAgentProfileInput) -> RawResponse[models.AgentProfile]: + "Get an agent profile\n\nReturns the agent profile belonging to the identified project. The profile is project-scoped and is distinct from the authenticated account's personal profile. Use the dedicated avatar operations when uploading or removing an agent avatar." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_AGENT_PROFILE, payload) + + async def reset_avatar( + self, input: ResetAgentProfileAvatarInput + ) -> RawResponse[models.AgentProfile]: + "Reset an agent avatar\n\nReplaces the selected project's agent avatar with the project's default avatar, a generated planet image derived from the project ID, and returns the updated agent profile. The reset does not restore an earlier avatar: a custom avatar it replaces is discarded and must be uploaded and committed again to use it. When the default avatar is already in use, the profile is returned unchanged. This does not change the account's personal profile picture. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RESET_AGENT_PROFILE_AVATAR, payload) + + async def update(self, input: UpdateAgentProfileInput) -> RawResponse[models.AgentProfile]: + "Update an agent profile\n\nUpdates the supplied firstName and lastName fields in the project's agent profile and returns the updated profile. Avatar upload, commit and reset are separate operations. The caller must be authorized to change configuration for the selected project. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_AGENT_PROFILE, payload) + + +class AsyncRawProjectsBillingResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def get_operation( + self, input: GetBillingOperationInput + ) -> RawResponse[models.BillingOperation]: + "Get a billing operation snapshot\n\nReturns the authoritative state of a billing operation belonging to the selected project. Use it to recover or poll a plan-change request until the operation reaches success or failure. An accepted request is not evidence that the plan change has completed." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_BILLING_OPERATION, payload) + + async def get_overview( + self, input: GetBillingOverviewInput + ) -> RawResponse[models.GetBillingOverviewResponse]: + "Get the project's billing overview\n\nReturns the selected project's plan information, entitlements and current billing-period usage. This operation reads project billing state; it does not change plans or the payer's payment method. Organization-level plans are available through the organization billing overview." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_BILLING_OVERVIEW, payload) + + async def list_billing_plans( + self, input: ListBillingPlansInput + ) -> RawResponse[models.ListBillingPlansResponse]: + "List available billing plans\n\nLists the billing plan catalog, grouped by their public plan-metadata type. Use the returned plan information when choosing the category and planCode for a plan change. The catalog is the same for every project, and reading it does not purchase a plan." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_BILLING_PLANS, payload) + + +class AsyncRawProjectsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + self.agent_profile = AsyncRawProjectsAgentProfileResource(transport) + self.billing = AsyncRawProjectsBillingResource(transport) + self.platforms = AsyncRawProjectsPlatformsResource(transport) + + async def create_project_api_key( + self, input: CreateProjectApiKeyInput + ) -> RawResponse[models.CreateProjectApiKeyResponse]: + "Create a project API key\n\nCreates a key bound to the selected project using the supplied name, permissions and optional expiry. The secret is returned only in this response and in idempotent replays of it; store it securely because other reads never return it. The key is scoped to this project and does not grant account-level access. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_PROJECT_API_KEY, payload) + + async def create_webhook_destination( + self, input: CreateWebhookDestinationInput + ) -> RawResponse[models.CreateWebhookDestinationResponse]: + "Create a webhook destination\n\nCreates a webhook destination for the selected project using its URL, payload API version, event selection and other documented settings. The response includes the signing secret, which is returned only in this response and in idempotent replays of it, never by destination reads; store it securely for signature verification. The API version must be selectable and selected event types must belong to that version's catalog. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_WEBHOOK_DESTINATION, payload) + + async def delete(self, input: DeleteProjectInput) -> RawResponse[models.Project]: + "Delete a project\n\nStarts deletion of the identified project using a credential authorized for project management. Inspect the documented response and use getProjectClosureStatus with the organization and project identifiers to read closure progress. A project API key is not an accepted credential for this operation." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DELETE_PROJECT, payload) + + async def delete_webhook_destination( + self, input: DeleteWebhookDestinationInput + ) -> RawResponse[models.WebhookDestination]: + "Delete a webhook destination\n\nDeletes the selected project's destination and returns its stable tombstone. Repeated deletion returns the deletion representation. This operation removes the destination configuration; it is separate from disabling a destination through an update." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DELETE_WEBHOOK_DESTINATION, payload) + + async def download_attachment(self, input: DownloadAttachmentInput) -> RawResponse[bytes]: + "Download an Attachment\n\nDownloads an Attachment's bytes. If unavailable after ten seconds, returns ATTACHMENT_NOT_READY with Retry-After: 5." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DOWNLOAD_ATTACHMENT, payload) + + async def get(self, input: GetProjectInput) -> RawResponse[models.Project]: + "Get a project\n\nReturns the identified project's settings for an authorized caller. The credential must be allowed to access that project; possession of an unrelated project's key does not provide access. Missing and deleted projects are reported through the documented error responses." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_PROJECT, payload) + + async def get_attachment(self, input: GetAttachmentInput) -> RawResponse[models.Attachment]: + "Get an Attachment\n\nReturns an Attachment's metadata. Use the content endpoint to download its bytes." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_ATTACHMENT, payload) + + async def get_message_metrics_backfill( + self, input: GetMessageMetricsBackfillInput + ) -> RawResponse[models.GetMessageMetricsBackfillResponse]: + "Get Metrics historical backfill status\n\nReturns historical metrics update progress. Completion reflects lastVerifiedAt; queries remain available during updates." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_MESSAGE_METRICS_BACKFILL, payload) + + async def get_message_metrics_sql_schema( + self, input: GetMessageMetricsSqlSchemaInput + ) -> RawResponse[models.GetMessageMetricsSqlSchemaResponse]: + "Get messaging and voice metrics SQL schema\n\nReturns the message_events SQL schema, supported queries, and limits for the selected API version." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_MESSAGE_METRICS_SQL_SCHEMA, payload) + + async def get_webhook_destination( + self, input: GetWebhookDestinationInput + ) -> RawResponse[models.WebhookDestination]: + "Get a webhook destination\n\nReturns the configuration of one webhook destination belonging to the selected project. Missing or deleted destinations are reported as errors. This read does not disclose the signing secret returned when the destination or a secret rotation was created." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WEBHOOK_DESTINATION, payload) + + async def get_webhook_event_schema( + self, input: GetWebhookEventSchemaInput + ) -> RawResponse[models.WebhookEventSchema | None]: + "Get a webhook event schema\n\nReturns the published reader JSON Schema for eventType in the requested webhook apiVersion. Use it to interpret events for that exact payload version. The response media type is application/schema+json; an authorized conditional request may return 304 without a body. Unsupported event/version combinations are rejected." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_WEBHOOK_EVENT_SCHEMA, payload) + + async def list_attachments( self, input: ListAttachmentsInput - ) -> models.AttachmentPage | RawResponse[models.AttachmentPage]: + ) -> RawResponse[models.AttachmentPage]: + "List Project Attachments\n\nLists the Project's Attachment metadata, with optional time filters." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_ATTACHMENTS, payload) + + async def list_project_api_keys( + self, input: ListProjectApiKeysInput + ) -> RawResponse[models.ListProjectApiKeysResponse]: + "List project API keys\n\nLists the API keys on the selected project, ordered newest first. Revoked keys are not listed; expired keys stay listed until they are revoked. Entries contain key metadata and permissions, never secret values. Use the returned identifiers to manage an existing key; lost secrets cannot be recovered through this operation." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_PROJECT_API_KEYS, payload) + + async def list_webhook_api_versions( + self, input: ListWebhookApiVersionsInput + ) -> RawResponse[models.ListWebhookApiVersionsResponse | None]: + "List webhook API versions\n\nLists the published webhook payload API versions and their lifecycle metadata. The list is the same for every project. Use the selectable indicator when choosing a version for a destination. These payload dates are separate from SDK package versions. An authorized conditional request may return 304 without a response body." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WEBHOOK_API_VERSIONS, payload) + + async def list_webhook_destinations( + self, input: ListWebhookDestinationsInput + ) -> RawResponse[models.WebhookDestinationPage]: + "List webhook destinations\n\nReturns a cursor-paginated page of active webhook destinations configured for the selected project. Use pageSize and pageToken to navigate it. The listing returns destination configuration, never signing secrets." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WEBHOOK_DESTINATIONS, payload) + + async def list_webhook_egress_addresses( + self, input: ListWebhookEgressAddressesInput + ) -> RawResponse[models.ListWebhookEgressAddressesResponse | None]: + "List webhook egress addresses\n\nReturns the public network addresses from which this environment sends webhook deliveries. Use this information when configuring the receiving system's network allowlist. The result is environment-specific and does not describe the API service's ingress addresses." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WEBHOOK_EGRESS_ADDRESSES, payload) + + async def list_webhook_event_types( + self, input: ListWebhookEventTypesInput + ) -> RawResponse[models.ListWebhookEventTypesResponse | None]: + "List webhook event types\n\nLists the webhook event types available in the requested apiVersion, including their descriptions, audiences and reader-schema URLs. Use this versioned catalog when selecting a destination's enabledEvents. The response may include version-retirement information; an authorized conditional request can return 304 without a body." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_WEBHOOK_EVENT_TYPES, payload) + + async def query_message_metrics( + self, input: QueryMessageMetricsInput + ) -> RawResponse[models.QueryMessageMetricsResponse]: + "Query messaging and voice metrics with SQL\n\nRuns read-only SQL over the Project's message_events table. Get the SQL schema for supported columns, capabilities, and limits." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_QUERY_MESSAGE_METRICS, payload) + + async def revoke_project_api_key( + self, input: RevokeProjectApiKeyInput + ) -> RawResponse[models.ProjectApiKeyResponse]: + "Delete a project API key\n\nRevokes the identified key on the selected project and returns its revoked metadata. Repeating the deletion returns the same revokedAt value. This operation does not rotate the key or return a replacement secret. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REVOKE_PROJECT_API_KEY, payload) + + async def rotate_webhook_signing_secret( + self, input: RotateWebhookSigningSecretInput + ) -> RawResponse[models.RotateWebhookSigningSecretResponse]: + "Rotate a webhook signing secret\n\nRotates the signing secret for the selected project's webhook destination and returns the new secret. The optional overlapSeconds controls the requested overlap with the previous secret according to the documented request constraints. Store the new secret securely and update the receiver's signature verification configuration; it is returned only in this response and in idempotent replays of it, never by destination reads. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_ROTATE_WEBHOOK_SIGNING_SECRET, payload) + + async def update(self, input: UpdateProjectInput) -> RawResponse[models.Project]: + "Update a project\n\nUpdates the identified project's name and returns the updated project. The project slug is not a mutable field in this request. Use an authorized account or organization service-identity credential; a project API key is not accepted. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_PROJECT, payload) + + async def update_project_api_key( + self, input: UpdateProjectApiKeyInput + ) -> RawResponse[models.ProjectApiKeyResponse]: + "Update a project API key's permissions\n\nReplaces the identified project key's permission list with the supplied permissions and returns the updated metadata. Sending the permission list the key already has leaves it unchanged. This request does not create a new secret or change the key's project binding. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_PROJECT_API_KEY, payload) + + async def update_webhook_destination( + self, input: UpdateWebhookDestinationInput + ) -> RawResponse[models.WebhookDestination]: + "Update a webhook destination\n\nUpdates the supplied URL, name, description, status or enabledEvents fields on a project's webhook destination and returns its updated configuration. The payload API version is not a mutable field in this request. Event selections are checked against the destination's versioned catalog; signing-secret rotation is a separate operation. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_WEBHOOK_DESTINATION, payload) + + async def upload_attachment( + self, input: UploadAttachmentInput + ) -> RawResponse[models.Attachment]: + "Upload an Attachment\n\nUploads a file and returns its Attachment once ready to download." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True, exclude={"body"}) + payload["body"] = input.body.root if input.body is not None else None + return await self._transport.request(_OP_UPLOAD_ATTACHMENT, payload) + + +class AsyncRawAuthDeviceResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def authorize( + self, input: DeviceAuthorizeInput | None = None + ) -> RawResponse[models.DeviceAuthorizeResponse]: + "Start Device Authorization\n\nStarts the device authorization flow for a CLI or another device without a browser. Show the verification URL and user code, then poll the token endpoint at the returned interval. No request fields are required; any supplied body is ignored." + payload = (input or DeviceAuthorizeInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_DEVICE_AUTHORIZE, payload) + + async def token(self, input: DeviceTokenInput) -> RawResponse[models.DeviceTokenResponse]: + "Exchange Device Code or Refresh Token\n\nExchanges an authorized device code or a refresh token for an access token and rotating refresh token. Accepts JSON and form-encoded bodies. While polling, wait at least interval seconds and increase the interval on slow_down. Store the new refresh token after every successful grant.\n\nThis SDK method sends uncompressed JSON (application/json). Other request formats described above apply to direct HTTP requests." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DEVICE_TOKEN, payload) + + +class AsyncRawAuthResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + self.device = AsyncRawAuthDeviceResource(transport) + + async def begin_invitation_sso( + self, input: BeginInvitationSsoInput + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: + "Authenticate to an invitation's organization SSO connection\n\nReturns an authentication URL for the organization SSO connection associated with the supplied invitation token. Supply token and returnTo. Complete the returned authentication flow; requesting its URL does not itself accept the invitation." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_BEGIN_INVITATION_SSO, payload) + + async def begin_organization_authentication( + self, input: BeginOrganizationAuthenticationInput + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: + "Authenticate to the current organization SSO connection\n\nReturns a URL to authenticate through the selected organization’s current SSO connection. Supply returnTo and open the returned URL to continue the flow. Receiving the URL does not establish an authenticated session." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_BEGIN_ORGANIZATION_AUTHENTICATION, payload) + + async def begin_organization_closure_authentication( + self, input: BeginOrganizationClosureAuthenticationInput + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: + "Authenticate the current Owner to inspect organization closure\n\nReturns an authentication URL for the current organization owner to inspect organization closure. Supply returnTo and complete the returned flow. This operation initiates authentication and does not close the organization." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_BEGIN_ORGANIZATION_CLOSURE_AUTHENTICATION, payload) + + async def begin_organization_sso_admission( + self, input: BeginOrganizationSsoAdmissionInput + ) -> RawResponse[models.OrganizationAuthenticationRedirect]: + "Begin organization SSO admission for an existing Account\n\nReturns an SSO admission URL for an existing account and the selected organization. Supply returnTo for the continuation URL. Admission requires completing the returned authentication flow; creating the URL does not itself grant membership." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_BEGIN_ORGANIZATION_SSO_ADMISSION, payload) + + async def create_organization_sso_portal_link( + self, input: CreateOrganizationSsoPortalLinkInput + ) -> RawResponse[models.CreateOrganizationSsoPortalLinkResponse]: + "Create organization SSO setup portal\n\nReturns an organization setup portal URL. Supply returnTo and optionally intent, either sso or domain_verification; sso is the default. Open the returned URL to complete the selected setup flow." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_ORGANIZATION_SSO_PORTAL_LINK, payload) + + async def disable_organization_sso( + self, input: DisableOrganizationSsoInput + ) -> RawResponse[models.OrganizationSsoConfiguration]: + "Turn organization SSO off\n\nDeletes the provider connection, releases the SSO requirement once the connection is gone, then unbinds the chosen domains. Retry with the same Idempotency-Key to resume or await the same run." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_DISABLE_ORGANIZATION_SSO, payload) + + async def get_organization_connection_status( + self, input: GetOrganizationConnectionStatusInput + ) -> RawResponse[models.OrganizationConnectionStatus]: + "Read organization and own membership synchronization\n\nRequires current human organization membership. Synchronization status does not attest SSO configuration or completed authorization." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_ORGANIZATION_CONNECTION_STATUS, payload) + + async def get_organization_sso_configuration( + self, input: GetOrganizationSsoConfigurationInput + ) -> RawResponse[models.OrganizationSsoConfiguration]: + "Read organization SSO configuration\n\nReturns the selected organization’s SSO connection state, configuration version, and desired and effective policy settings. Read policySyncStatus alongside the enforcement fields to distinguish requested settings from synchronized settings." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_ORGANIZATION_SSO_CONFIGURATION, payload) + + async def list_oauth_scopes( + self, input: ListOauthScopesInput | None = None + ) -> RawResponse[models.ListOauthScopesResponse]: + "List OAuth scopes\n\nLists the business permissions available to OAuth applications." + payload = (input or ListOauthScopesInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_LIST_OAUTH_SCOPES, payload) + + async def refresh_organization_sso_connection( + self, input: RefreshOrganizationSsoConnectionInput + ) -> RawResponse[models.OrganizationSsoConfiguration]: + "Refresh organization SSO connection\n\nRefreshes the selected organization’s SSO connection and returns its current connection state, configuration version and policy synchronization status. This operation takes no request body." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REFRESH_ORGANIZATION_SSO_CONNECTION, payload) + + async def retry_organization_connection_sync( + self, input: RetryOrganizationConnectionSyncInput + ) -> RawResponse[models.OrganizationConnectionStatus]: + "Retry own organization connection synchronization\n\nReconciles existing local intent. Takes no body and cannot change membership, roles or authentication policy." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RETRY_ORGANIZATION_CONNECTION_SYNC, payload) + + async def start_enterprise_login( + self, input: StartEnterpriseLoginInput + ) -> RawResponse[models.StartEnterpriseLoginResponse]: + "Start company sign-in without an existing Account\n\nReturns a sign-in URL without requiring an existing account: the company SSO connection when the target has a ready connection, otherwise ordinary account login. Supply one documented enrollment variant: organizationId with returnTo (optionally invitationToken), invitationToken with returnTo, or retryToken. Open the returned URL to continue authentication; receiving a URL does not complete sign-in. This is a browser flow: the request must come from an allowed Origin, and the retryToken variant also needs the retry cookie set by the failed sign-in, so send it with credentials." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_START_ENTERPRISE_LOGIN, payload) + + async def update_organization_sso_policy( + self, input: UpdateOrganizationSsoPolicyInput + ) -> RawResponse[models.OrganizationSsoConfiguration]: + "Update organization SSO policy\n\nUpdates whether SSO can admit new members automatically using ssoJitEnabled and the current expectedVersion. Returns the organization’s SSO configuration and policy synchronization status; a successful response does not mean every desired policy setting has finished synchronizing." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_ORGANIZATION_SSO_POLICY, payload) + + +class AsyncRawOrganizationsBillingResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def cancel_subscription( + self, input: CancelSubscriptionInput + ) -> RawResponse[models.CancelSubscriptionResponse]: + "Cancel a category at the end of its billing period\n\nSchedules cancellation of the specified project's billing category at the end of its current period. The category remains active through the returned cancelsAt instant and then stops renewing. This is a scheduled cancellation, not an immediate removal of the remaining period's service. If the category has no active subscription, nothing changes and the response has cancellationScheduled set to false and cancelsAt set to null. Supply both organizationId and projectId to select the project within its organization." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CANCEL_SUBSCRIPTION, payload) + + async def change_plan( + self, input: ChangePlanInput + ) -> RawResponse[models.TerminalBillingOperation | models.PendingBillingOperation]: + "Purchase or change a category's plan\n\nPurchases or changes the selected project's plan for the supplied category and planCode. A 202 response means the change is pending: poll the returned operation URL and honor Retry-After until it succeeds or fails. A 200 response means the idempotency key resolved to an operation that is already terminal; inspect that result rather than assuming success from the status code alone. Supply the required Idempotency-Key header. Supply both organizationId and projectId to select the project within its organization." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CHANGE_PLAN, payload) + + async def create_organization_payment_method_checkout( + self, input: CreateOrganizationPaymentMethodCheckoutInput + ) -> RawResponse[models.CreateOrganizationPaymentMethodCheckoutResponse]: + "Get a payment-method checkout URL for the organization\n\nReturns a hosted payment-method collection URL for the selected organization. An Idempotency-Key header is optional; supply one to make retries safe. Complete the returned checkout flow. Receiving the URL does not mean a card has been saved; check payment-method status afterward." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request( + _OP_CREATE_ORGANIZATION_PAYMENT_METHOD_CHECKOUT, payload + ) + + async def create_organization_setup_intent( + self, input: CreateOrganizationSetupIntentInput + ) -> RawResponse[models.CreateOrganizationSetupIntentResponse]: + "Create a SetupIntent for an in-app card capture\n\nCreates payment-provider configuration for collecting a card for the selected organization and returns clientSecret and publishableKey. Supply the required Idempotency-Key header. Complete the provider’s card-collection flow separately and avoid logging the returned client secret." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_ORGANIZATION_SETUP_INTENT, payload) + + async def get_organization_billing_overview( + self, input: GetOrganizationBillingOverviewInput + ) -> RawResponse[models.GetOrganizationBillingOverviewResponse]: + "Get the organization's billing overview\n\nReturns the selected organization’s billing subscription and entitlementsVersion. The subscription can be null. Read the returned plan, charges and entitlements to inspect organization billing; this operation does not purchase or change a plan." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_ORGANIZATION_BILLING_OVERVIEW, payload) + + async def get_organization_payment_method( + self, input: GetOrganizationPaymentMethodInput + ) -> RawResponse[models.GetOrganizationPaymentMethodResponse]: + "Check the organization for a card on file\n\nReports whether the selected organization has a card on file and returns its documented payment-method metadata. Reading this endpoint does not collect a new card or create a checkout session." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_ORGANIZATION_PAYMENT_METHOD, payload) + + async def list_invoices( + self, input: ListInvoicesInput + ) -> RawResponse[models.ListInvoicesResponse]: + "List invoices\n\nReturns a single page of the selected organization's invoices; invoices with a zero total are excluded. Use the documented invoice fields to inspect each invoice's billing state. Listing invoices does not make a payment or modify a subscription." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_INVOICES, payload) + + async def resume_subscription( + self, input: ResumeSubscriptionInput + ) -> RawResponse[models.ResumeSubscriptionResponse]: + "Resume a category scheduled for cancellation\n\nRemoves a scheduled cancellation for the specified billing category on the selected project so it can renew normally. This operation resumes a category scheduled to cancel; it is separate from purchasing or changing a plan. Supply both organizationId and projectId to select the project within its organization." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RESUME_SUBSCRIPTION, payload) + + +class AsyncRawOrganizationsProjectsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def check_project_slug_availability( + self, input: CheckProjectSlugAvailabilityInput + ) -> RawResponse[models.CheckProjectSlugAvailabilityResponse]: + "Check slug availability\n\nReports whether createProject would accept `slug` right now. Advisory: only the create itself allocates, so a caller must still handle SLUG_TAKEN. A malformed slug is rejected on shape; a reserved slug, a slug held by an active project, and a slug retired with a deleted project each answer `available: false` with a reason." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CHECK_PROJECT_SLUG_AVAILABILITY, payload) + + async def count(self, input: CountProjectsInput) -> RawResponse[models.ProjectCount]: + "Count accessible projects\n\nCounts the projects the same filter would list. The count is read from the primary, so it is authoritative rather than replica-lagged." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_COUNT_PROJECTS, payload) + + async def create(self, input: CreateProjectInput) -> RawResponse[models.Project]: + "Create a project\n\nCreates in the authorized organization. In addition to account credentials, explicitly granted Service Identity API keys and M2M tokens may create projects. Project API keys cannot create projects. Creator and private credential evidence come only from the trusted authorization context. The caller-selected slug is immutable, must be 3 to 63 lowercase ASCII alphanumerics separated by single hyphens, and cannot be reserved or held by any active or deleted project." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_PROJECT, payload) + + async def get_project_closure_status( + self, input: GetProjectClosureStatusInput + ) -> RawResponse[models.GetProjectClosureStatusResponse]: + "Read project closure progress\n\nReturns closure progress for projectId within organizationId, including deletionOperationId, domain progress and ready. This read operation does not initiate deletion." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_GET_PROJECT_CLOSURE_STATUS, payload) + + async def list(self, input: ListProjectsInput) -> RawResponse[models.ProjectPage]: + "List accessible projects\n\nReturns a cursor-paginated page of the active projects in organizationId. Filter using query and the documented creation-time bounds, and navigate with pageSize and pageToken. Project roles are not returned and role is not a supported filter." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_LIST_PROJECTS, payload) + + +class AsyncRawOrganizationsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + self.billing = AsyncRawOrganizationsBillingResource(transport) + self.projects = AsyncRawOrganizationsProjectsResource(transport) + + +class AsyncRawAccountResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def commit_profile_picture( + self, input: CommitAccountProfilePictureInput + ) -> RawResponse[models.Account]: + "Commit a profile picture\n\nCommits a profile picture previously uploaded through createAccountProfilePictureUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's ownership, size, content type, image bytes, dimensions, encryption, and age before changing the Account." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_COMMIT_ACCOUNT_PROFILE_PICTURE, payload) + + async def confirm_phone_verification( + self, input: ConfirmAccountPhoneVerificationInput + ) -> RawResponse[models.Account]: + "Confirm a phone number verification\n\nBinds the number once the code is approved. Repeat calls return the bound Account." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CONFIRM_ACCOUNT_PHONE_VERIFICATION, payload) + + async def create_account_service_key( + self, input: CreateAccountServiceKeyInput + ) -> RawResponse[models.CreateAccountServiceKeyResponse]: + "Create an Account Service Key\n\nCreates a service key for the authenticated account with the supplied name and optional expiresAt. Returns key metadata and a one-time credential; store the credential securely because it cannot be retrieved through the listing endpoint. These credentials act as the account and must not be distributed as project-scoped keys. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_ACCOUNT_SERVICE_KEY, payload) + + async def create_profile_picture_upload( + self, input: CreateAccountProfilePictureUploadInput + ) -> RawResponse[models.ProfilePictureUpload]: + "Create a profile picture upload\n\nCreates a ten-minute, Account-bound presigned S3 POST for a JPEG, PNG, or WebP profile picture up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAccountProfilePicture with the returned uploadId. Do not cache or log the upload URL or form fields." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_ACCOUNT_PROFILE_PICTURE_UPLOAD, payload) + + async def delete(self, input: DeleteAccountInput | None = None) -> RawResponse[models.Account]: + "Delete the authenticated account\n\nDeletes the authenticated account and returns its account tombstone. The operation is rejected while the account still owns organizations; transfer or close those organizations before retrying. This endpoint acts on the caller's account and does not accept another account's identifier." + payload = (input or DeleteAccountInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_DELETE_ACCOUNT, payload) + + async def get(self, input: GetAccountInput | None = None) -> RawResponse[models.Account]: + "Get the authenticated account\n\nReturns the profile of the authenticated account. The account is selected from the credential rather than a request parameter. A missing or deleted account is reported as an error instead of an empty profile." + payload = (input or GetAccountInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_GET_ACCOUNT, payload) + + async def list_account_service_keys( + self, input: ListAccountServiceKeysInput | None = None + ) -> RawResponse[models.ListAccountServiceKeysResponse]: + "List Account Service Keys\n\nReturns metadata for the authenticated account's unrevoked service keys, including expired keys, ordered newest first. Secret values are not returned; a key's credential is disclosed only when that key is created." + payload = (input or ListAccountServiceKeysInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_LIST_ACCOUNT_SERVICE_KEYS, payload) + + async def list_authorized_applications( + self, input: ListAuthorizedApplicationsInput | None = None + ) -> RawResponse[models.ListAuthorizedApplicationsResponse]: + "List connected applications\n\nLists the OAuth applications authorized by the authenticated user." + payload = (input or ListAuthorizedApplicationsInput()).model_dump( + mode="json", by_alias=True, exclude_unset=True + ) + return await self._transport.request(_OP_LIST_AUTHORIZED_APPLICATIONS, payload) + + async def reset_profile_picture( + self, input: ResetAccountProfilePictureInput + ) -> RawResponse[models.Account]: + "Remove a profile picture\n\nRemoves the authenticated account's custom profile picture and returns the account using its default picture. This operation does not upload a replacement; use the upload-and-commit operations when setting a new custom picture. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_RESET_ACCOUNT_PROFILE_PICTURE, payload) + + async def revoke_account_service_key( + self, input: RevokeAccountServiceKeyInput + ) -> RawResponse[models.RevokeAccountServiceKeyResponse]: + "Revoke an Account Service Key\n\nRevokes the account-owned service key identified by serviceKeyId and returns its revoked metadata. Repeating the revocation is stable. Revocation changes the credential's validity; it does not create a replacement key. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REVOKE_ACCOUNT_SERVICE_KEY, payload) + + async def revoke_authorized_application( + self, input: RevokeAuthorizedApplicationInput + ) -> RawResponse[None]: + "Revoke a connected application\n\nRevokes the authenticated user's grant for one OAuth application." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REVOKE_AUTHORIZED_APPLICATION, payload) + + async def start_phone_verification( + self, input: StartAccountPhoneVerificationInput + ) -> RawResponse[models.StartAccountPhoneVerificationResponse]: + "Start a phone number verification\n\nSends an SMS code. Answers CAPTCHA_REQUIRED with the widget to render when no solved challenge accompanies the request; retry with the returned challengeContext and a token. Rate limited per account, per destination number, and globally; a rejection carries Retry-After." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_START_ACCOUNT_PHONE_VERIFICATION, payload) + + async def update(self, input: UpdateAccountInput) -> RawResponse[models.Account]: + "Update the authenticated account\n\nUpdates the supplied firstName and lastName fields on the authenticated account and returns the updated profile. Only the documented profile fields can be changed through this endpoint; profile-picture uploads and phone-number verification use their dedicated operations. Supply the required Idempotency-Key header." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_UPDATE_ACCOUNT, payload) + + +class AsyncRawSystemResource: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def create_app_installation_request( + self, input: CreateAppInstallationRequestInput + ) -> RawResponse[models.CreateAppInstallationRequestResponse]: + "Request an app installation\n\nAuthenticates a registered app backend using a short-lived signed client assertion. Creates request metadata only; customer approval is still required." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_CREATE_APP_INSTALLATION_REQUEST, payload) + + async def redeem_app_installation_delivery( + self, input: RedeemAppInstallationDeliveryInput + ) -> RawResponse[models.RedeemAppInstallationDeliveryResponse]: + "Redeem an approved installation credential\n\nThe registered app backend authenticates with a signed client assertion and a single-use code. Plaintext is returned only once; retries return status and never create another credential." + payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) + return await self._transport.request(_OP_REDEEM_APP_INSTALLATION_DELIVERY, payload) + + +class AsyncProjectsPlatformsImessageAssignmentsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsPlatformsImessageAssignmentsResource(transport) + + async def create(self, input: CreateSharedLineAssignmentInput) -> models.SharedLineAssignment: + "Create shared line assignment\n\nMaps an end user's iMessage handle — an E.164 phone number or an email address — onto one of the project's pooled shared iMessage lines, consuming a seat from the project's entitlement. The assigned number is allocated by the server. When an email address is supplied in `email` the user is sent an invite asynchronously to that address; it is never inferred from the handle, and the response never reports whether the send succeeded. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.create(input)).data + + async def get(self, input: GetSharedLineAssignmentInput) -> models.SharedLineAssignment: + "Get shared line assignment\n\nReads one shared line assignment. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get(input)).data + + async def list(self, input: ListSharedLineAssignmentsInput) -> models.SharedLineAssignmentPage: + "List shared line assignments\n\nLists the project's shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.list(input)).data + + async def release(self, input: ReleaseSharedLineAssignmentInput) -> models.SharedLineAssignment: + "Release shared line assignment\n\nReleases a shared line assignment, freeing both its seat and its handle for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.release(input)).data + + +class AsyncProjectsPlatformsImessageResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsPlatformsImessageResource(transport) + self.assignments = AsyncProjectsPlatformsImessageAssignmentsResource(transport) + + +class AsyncProjectsPlatformsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsPlatformsResource(transport) + self.imessage = AsyncProjectsPlatformsImessageResource(transport) + + async def assign_sms_line_campaign(self, input: AssignSmsLineCampaignInput) -> models.Operation: + "Assign or replace SMS line campaign\n\nAttach a ready campaign from this project’s organization to its line. Requires platforms:write for the project; human and machine actors retain their authenticated identity. Requires a permanent Idempotency-Key and the current assignment version. Provider provisioning runs asynchronously." + return (await self._raw_resource.assign_sms_line_campaign(input)).data + + async def assign_voice_line_profile( + self, input: AssignVoiceLineProfileInput + ) -> models.VoiceLineProfileAssignment: + "Assign Voice line profile\n\nAssigns or replaces a line's explicit additional-profile override when the resource version matches. The current default cannot be assigned explicitly. The pstn_voice ability remains the admission source of truth. Requires platforms:write bound to the path project." + return (await self._raw_resource.assign_voice_line_profile(input)).data + + async def batch_update_voice_line_profile_assignments( + self, input: BatchUpdateVoiceLineProfileAssignmentsInput + ) -> models.BatchUpdateVoiceLineProfileAssignmentsResponse: + "Batch update Voice line profile assignments\n\nAtomically sets additional-profile overrides or switches lines back to the project default for up to 100 Voice-capable lines. A null profileId means use the default. Every expected resource version must match or no line changes. Requires platforms:write bound to the path project." + return (await self._raw_resource.batch_update_voice_line_profile_assignments(input)).data + + async def cancel_operation(self, input: CancelOperationInput) -> models.Operation: + "Cancel operation\n\nWithdraws a provision that has not been fulfilled yet. This is an operations action rather than a DELETE, because there is nothing to delete: no resource exists until the work commits. Whether it is accepted depends on the resource type — a dedicated iMessage line may sit waiting on inventory for hours and withdrawing costs nothing, while an SMS number is cancellable during inventory waiting and answers 409 once the workflow commits to its first provider order. Campaign assignment and detachment operations cannot be cancelled in any state. Wait for completion before requesting another change; that new change is not a guaranteed rollback. The output-only `cancellable` field is a snapshot; the cancellation transaction always checks the current phase under a row lock. A cancel that loses the race against the work finishing also answers 409: the resource exists and is billed for, so what you want then is to release it. Nothing is charged for a cancelled provision — billing runs after the work, so there is never anything to refund. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.cancel_operation(input)).data + + async def configure_voice_profile_outbound( + self, input: ConfigureVoiceProfileOutboundInput + ) -> models.ConfigureVoiceProfileOutboundResponse: + "Configure Voice profile outbound credential\n\nConfigures a SIP credential for outbound calls from a profile when the shared profile version matches. authentication.algorithm is required: SHA-256 is recommended, while MD5 is a weaker legacy option supported over UDP, TCP, and TLS; TLS is strongly recommended because UDP and TCP do not encrypt SIP signaling. The profileId may identify the default or an additional profile. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." + return (await self._raw_resource.configure_voice_profile_outbound(input)).data + + async def connect_email_domain(self, input: ConnectEmailDomainInput) -> models.Operation: + "Connect email domain\n\nReserves a normalized DNS domain and starts its durable email-provider setup. The accepted provision consumes one email-domain entitlement slot until it fails, is cancelled, or becomes a live resource; the plan's email.max_email_domains value sets the project limit. The customer resource does not exist until provider identity and DNS setup reach READY; poll the returned operation for progress. A domain may have only one unfinished provision or live resource globally. Email domains have no additional per-domain charge. The Idempotency-Key is required and permanent: replaying the same key and canonical domain returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.connect_email_domain(input)).data + + async def connect_telegram_bot(self, input: ConnectTelegramBotInput) -> models.Operation: + "Connect Telegram bot\n\nStarts a free managed Telegram bot connection. Each project may have one unfinished Telegram provision, including user interaction and failure cleanup. A different Idempotency-Key while one is active returns 409 TELEGRAM_PROVISION_IN_PROGRESS with its operationId and operationUrl; resume it, cancel it while cancellation is available, or wait for it to finish. Rejected keys remain reusable. Open detail.setupUrl to connect an existing managed bot or create a new one with the project's default agent name or a custom display name, then poll Location. The link remains usable while the operation is active and never expires. Replaying the same Idempotency-Key returns the original operation, even after completion or while a newer setup is active. POST, GET and list share the same operation details. The API includes detail.setupUrl only for callers with platforms:write for the project; read-only callers receive the other details unchanged." + return (await self._raw_resource.connect_telegram_bot(input)).data + + async def connect_whatsapp_business( + self, input: ConnectWhatsappBusinessInput + ) -> models.Operation: + "Connect WhatsApp Business\n\nExchanges the authorization code Embedded Signup returned and connects exactly the selected phone number as one `whatsapp_sender`. Send the WABA id and phone-number id emitted by the same popup attempt; both are treated as selectors and verified against Meta before use. A selected number that matches a non-retired, same-project `voip_line` is linked to it; a number absent from Photon inventory stays unbound; a matching non-retired `cosmos_line`, foreign VoIP line or unassigned VoIP line fails the operation before registration. Connecting is free — no plan requirement — but Billing must report the project's organization as ready with a payment method on file. The Idempotency-Key is required and permanent: replaying the same key returns the original operation forever. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.connect_whatsapp_business(input)).data + + async def create_default_voice_profile( + self, input: CreateDefaultVoiceProfileInput + ) -> models.VoiceProfile: + "Create default Voice profile\n\nCreates the project default Voice profile when absent. An identical replay returns the existing default without changing its version; a different existing default conflicts. Direction configuration is managed separately. Requires platforms:write bound to the path project." + return (await self._raw_resource.create_default_voice_profile(input)).data + + async def create_voice_profile(self, input: CreateVoiceProfileInput) -> models.VoiceProfile: + "Create Voice profile\n\nCreates a direction-neutral additional Voice profile. The project default must already exist. Requires platforms:write bound to the path project." + return (await self._raw_resource.create_voice_profile(input)).data + + async def create_whatsapp_shared_line_assignment( + self, input: CreateWhatsappSharedLineAssignmentInput + ) -> models.SharedLineAssignment: + "Create WhatsApp shared line assignment\n\nMaps an end user's phone number onto one of the project's pooled shared WhatsApp lines, consuming a seat from the project's WhatsApp entitlement. The assigned number is allocated by the server. When an email address is supplied the user is sent an invite asynchronously; the response never reports whether that succeeded. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.create_whatsapp_shared_line_assignment(input)).data + + async def create_whatsapp_voip_sender( + self, input: CreateWhatsappVoipSenderInput + ) -> models.Operation: + "Create a VoIP-backed WhatsApp sender\n\nRegisters an active, SMS-capable Photon VoIP line on this project's connected WhatsApp Business Account. The account is resolved server-side; callers never select a WABA. The platform creates or reuses the Meta number, requests and consumes the SMS ownership code internally, verifies it, and registers the sender. displayName is optional; when omitted the project agent profile name is snapshotted before acceptance. The VoIP line remains a separate resource and never receives the whatsapp_business ability." + return (await self._raw_resource.create_whatsapp_voip_sender(input)).data + + async def delete_voice_profile(self, input: DeleteVoiceProfileInput) -> None: + "Delete Voice profile\n\nDeletes an additional profile when expectedVersion matches. Assigned profiles require force=true, which atomically removes every stored override so affected lines follow the default. The default can never be deleted. Requires platforms:write bound to the path project." + return (await self._raw_resource.delete_voice_profile(input)).data + + async def delete_voice_profile_inbound( + self, input: DeleteVoiceProfileInboundInput + ) -> models.VoiceProfileInboundConfiguration: + "Remove Voice profile inbound configuration\n\nRemoves a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. The profile and its line assignments remain. Requires platforms:write bound to the path project." + return (await self._raw_resource.delete_voice_profile_inbound(input)).data + + async def delete_voice_profile_outbound( + self, input: DeleteVoiceProfileOutboundInput + ) -> models.DeleteVoiceProfileOutboundResponse: + "Revoke Voice profile outbound credential\n\nRevokes outbound calling for a profile when the shared profile version matches. The profileId may identify the default or an additional profile. The profile, inbound destination, and line assignments remain. Requires platforms:write bound to the path project." + return (await self._raw_resource.delete_voice_profile_outbound(input)).data + + async def disconnect_whatsapp_business_account( + self, input: DisconnectWhatsappBusinessAccountInput + ) -> models.Operation: + "Disconnect WhatsApp Business account and numbers\n\nDisconnects every attached WhatsApp sender, then unsubscribes our app and removes the project's business account connection. Photon VoIP lines and the numbers in Meta remain. Requires Idempotency-Key. Poll the returned operation; provider refusals appear as operation failures and retain the account for retry with a new key. New signups are blocked while disconnecting, and existing provisions must finish before this request can be accepted." + return (await self._raw_resource.disconnect_whatsapp_business_account(input)).data + + async def get_default_voice_profile( + self, input: GetDefaultVoiceProfileInput + ) -> models.VoiceProfile: + "Get default Voice profile\n\nGets the profile currently selected as the project default, including its optional inbound delivery state. Requires platforms:read bound to the path project." + return (await self._raw_resource.get_default_voice_profile(input)).data + + async def get_imessage( + self, input: GetProjectImessagePlatformInput + ) -> models.ProjectPlatformSettings: + "Get project iMessage platform\n\nReports whether the project is on shared or dedicated iMessage lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_imessage(input)).data + + async def get_operation(self, input: GetOperationInput) -> models.GetOperationResponse: + "Get operation\n\nReads one operation using the same operation representation as creation and list. The API includes detail.setupUrl only with platforms:write for this project. This is the polling endpoint every asynchronous request here points its Location at, and it resolves from the moment that request is accepted — an operation is committed before its work is dispatched, so there is no window in which the URL 404s. Poll until `state` is one of `succeeded`, `failed` or `cancelled`, pacing from the Retry-After the accepting response returned. While an email domain waits for DNS, `detail` always contains the manual records and may additionally contain `automaticSetup` with a signed provider URL to open separately. Once the operation has produced a resource, the response carries that resource too, so the poll that finishes is also the one that tells you what you got. `succeeded` means the work is done; billing runs behind it and is not something the caller waits on. Operations are never purged, so a 404 means the id was never this project's. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_operation(input)).data + + async def get_project_whatsapp_platform( + self, input: GetProjectWhatsappPlatformInput + ) -> models.ProjectPlatformSettings: + "Get project WhatsApp platform\n\nReports whether the project is on shared or dedicated WhatsApp lines, derived from its billing entitlements. Shared mode carries the seat cap. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_project_whatsapp_platform(input)).data + + async def get_resource(self, input: GetResourceInput) -> models.Resource: + "Get resource\n\nReads one resource the project holds. A released number stays readable and reads `retired`, because it remains part of this project's history. A dedicated line given back does NOT: returning it to inventory is what makes it claimable by someone else, so it answers 404 and the operation that returned it is the record that this project once held it. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_resource(input)).data + + async def get_sms_line_campaign_assignment( + self, input: GetSmsLineCampaignAssignmentInput + ) -> models.GetSmsLineCampaignAssignmentResponse: + "Read SMS line campaign assignment\n\nRead the last confirmed campaign and current eligibility. Follow changes through their operations. Eligibility is a control-plane assessment, not a delivery or recipient-consent guarantee." + return (await self._raw_resource.get_sms_line_campaign_assignment(input)).data + + async def get_voice_line_profile_assignment( + self, input: GetVoiceLineProfileAssignmentInput + ) -> models.VoiceLineProfileAssignment: + "Get Voice line profile assignment\n\nGets the explicit additional-profile override for an owned Voice-capable line. A line following the project default returns 200 without profileId. Requires platforms:read bound to the path project." + return (await self._raw_resource.get_voice_line_profile_assignment(input)).data + + async def get_voice_profile(self, input: GetVoiceProfileInput) -> models.VoiceProfile: + "Get Voice profile\n\nGets one reusable Voice profile, including its optional inbound delivery state. Requires platforms:read bound to the path project." + return (await self._raw_resource.get_voice_profile(input)).data + + async def get_whatsapp_business_account( + self, input: GetWhatsappBusinessAccountInput + ) -> models.WhatsappBusinessAccount: + "Get WhatsApp Business account\n\nGets the one WhatsApp Business Account this project has connected, with its number of live senders. Senders are resources and are listed by GET /platforms/resources?ability=whatsapp_business. The account is not a resource and carries no access token. Meta's retained numbers are listed separately by GET /platforms/whatsapp-business/account/phone-numbers. `subscribedAt` is absent until our app is attached to the account's webhooks. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_whatsapp_business_account(input)).data + + async def get_whatsapp_business_verification_code( + self, input: GetWhatsappBusinessVerificationCodeInput + ) -> models.GetWhatsappBusinessVerificationCodeResponse: + "Get WhatsApp Business verification code\n\nReturns the latest six-digit WhatsApp Business ownership code received by SMS for an active Photon VOIP number, but only when its provider timestamp is strictly newer than the required receivedAfter boundary. receivedAfter must be an RFC 3339 timestamp between this request's arrival time and two minutes before it; once it expires, restart Meta's verification flow with a new boundary. A missing newer code is a retryable 404 with Retry-After: 2. Poll after 2, 4, 8, then 10 seconds, applying ±20% jitter and capping later intervals at 10 seconds. Stop when the original boundary is two minutes old. Responses are never cached. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.get_whatsapp_business_verification_code(input)).data + + async def get_whatsapp_shared_line_assignment( + self, input: GetWhatsappSharedLineAssignmentInput + ) -> models.SharedLineAssignment: + "Get WhatsApp shared line assignment\n\nReads one WhatsApp shared line assignment. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_whatsapp_shared_line_assignment(input)).data + + async def get_whatsapp_signup_config( + self, input: GetWhatsappSignupConfigInput + ) -> models.GetWhatsappSignupConfigResponse: + "Get WhatsApp signup config\n\nReturns what the browser needs to open Meta's Embedded Signup popup: the Facebook Login for Business configuration id, the Graph version to run against, and the scopes it will request. Answered in-process rather than forwarded, so the first step of onboarding survives an outage of the private service. Pass `configId` to `FB.login` as `config_id` with `response_type: 'code'` and `override_default_response_type: true`. Do NOT add a `featureType` — omitting it is what keeps the phone-number screen in the flow, and `only_waba_sharing` produces an account with no number that cannot be provisioned. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.get_whatsapp_signup_config(input)).data + + async def list_number_area_codes( + self, input: ListNumberAreaCodesInput + ) -> models.ListNumberAreaCodesResponse: + "List supported number area codes\n\nLists current provider coverage for US local numbers, sorted and deduplicated. Coverage does not guarantee inventory carrying every required feature. New area-specific purchases must use a listed code; accepted purchases keep waiting if coverage later changes. Requires platforms:read for the path project." + return (await self._raw_resource.list_number_area_codes(input)).data + + async def list_number_countries( + self, input: ListNumberCountriesInput + ) -> models.ListNumberCountriesResponse: + "List supported number countries\n\nLists supported purchase countries independently of current provider inventory. Requires platforms:read for the path project." + return (await self._raw_resource.list_number_countries(input)).data + + async def list_operations(self, input: ListOperationsInput) -> models.OperationPage: + "List operations\n\nLists the project's operations using the same operation representation as creation and GET. The API includes detail.setupUrl only with platforms:write for this project. Results are oldest first — every provision and release it has ever asked for, including the ones still running. This is the entire in-flight view: a resource only appears once it is real, so nothing half-built shows up in the resource list and nothing in flight is missing from this one. Filter by `resourceId` to get one resource's whole history, which for a pooled line is every tenure this project has had on it. `state` is comma-separated; `type` accepts one operation type and an absent filter means everything, including failed and cancelled operations. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.list_operations(input)).data + + async def list_project_platforms( + self, input: ListProjectPlatformsInput + ) -> models.ListProjectPlatformsResponse: + "List project platforms\n\nLists the platform types available to this project. Every project currently sees the same fixed public contract, answered in-process rather than forwarded, so the list survives an outage of the private service. The project binding exists so that answer can narrow per project without moving the route. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.list_project_platforms(input)).data + + async def list_resources(self, input: ListResourcesInput) -> models.ResourcePage: + "List resources\n\nLists everything the project holds, oldest first, whatever kind of thing it is — one endpoint and one id shape for numbers, dedicated lines and whatever ships next. Nothing half-built appears here: a resource exists only once it is real, so anything still being provisioned is an operation rather than a resource with a pending flag. Filter by `type`, by `ability` (which matches only abilities that are currently enabled), and by `state` — comma-separated, and absent means every state, including retired ones. `detail` carries a per-type public view: an SMS number's number, a dedicated line's number and whether it is healthy. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.list_resources(input)).data + + async def list_voice_profiles(self, input: ListVoiceProfilesInput) -> models.VoiceProfilePage: + "List Voice profiles\n\nLists reusable Voice profiles in this project. Requires platforms:read bound to the path project." + return (await self._raw_resource.list_voice_profiles(input)).data + + async def list_whatsapp_account_phone_numbers( + self, input: ListWhatsappAccountPhoneNumbersInput + ) -> models.ListWhatsappAccountPhoneNumbersResponse: + "List WhatsApp account phone numbers\n\nLists the connected WABA's phone numbers directly from Meta, including numbers whose Photon sender was disconnected. Ownership is photon for a number in this project's current Photon inventory and meta otherwise. Match a Photon SMS number by its E.164 phoneNumber and reuse its existing displayName when reconnecting. A null name is unavailable, not permission to choose a new name. A failed lookup returns an error rather than an empty list. Requires platforms:read on the path project." + return (await self._raw_resource.list_whatsapp_account_phone_numbers(input)).data + + async def list_whatsapp_shared_line_assignments( + self, input: ListWhatsappSharedLineAssignmentsInput + ) -> models.SharedLineAssignmentPage: + "List WhatsApp shared line assignments\n\nLists the project's WhatsApp shared line assignments, oldest first. Released assignments are excluded unless includeReleased is set. Requires the platforms:read permission bound to the project resource in the path." + return (await self._raw_resource.list_whatsapp_shared_line_assignments(input)).data + + async def provision_imessage_dedicated_line( + self, input: ProvisionImessageDedicatedLineInput + ) -> models.Operation: + "Provision dedicated iMessage line\n\nClaims one dedicated iMessage line for the project and enables iMessage on it. Always answers 202 with an operation: dedicated lines are allocated from available capacity, and unavailable capacity causes a wait rather than a failure — this can legitimately stay `running` for hours, which is exactly why the response is a handle to poll rather than a number. The project's messaging subscription must grant the dedicated iMessage lines entitlement (`imessage_dedicated_lines.can_purchase`), and that is checked before capacity is reserved; nothing is charged until a line is actually claimed. If you no longer want to wait, POST to the operation's cancel endpoint, which costs nothing. The Idempotency-Key is required and permanent: repeating it returns the same operation forever. A further line always needs a NEW key, including while others are still waiting. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.provision_imessage_dedicated_line(input)).data + + async def provision_whatsapp_dedicated_line( + self, input: ProvisionWhatsappDedicatedLineInput + ) -> models.Operation: + "Provision dedicated WhatsApp line\n\nProvisions one dedicated WhatsApp line with WhatsApp and shared Voice enabled. It attaches to an eligible iMessage line the project already owns when possible so both products keep the same number; otherwise it claims healthy, available WhatsApp-capable dedicated-line inventory. Always answers 202, because waiting when no inventory is available is not a failure. The product opens its own charge period after the abilities are enabled; Voice has no separate charge. Cancel the returned operation to stop waiting. The Idempotency-Key is required and permanent." + return (await self._raw_resource.provision_whatsapp_dedicated_line(input)).data + + async def purchase_sms_number(self, input: PurchaseSmsNumberInput) -> models.Operation: + "Purchase SMS number\n\nBuys one US local number from the provider and records it as a resource with SMS enabled. Requires countryCode (US) and accepts an optional three-digit geographic areaCode. The server selects an exact matching number. Empty inventory keeps the operation running until a number is available or the caller cancels before ordering begins. Always answers 202 with an operation: the work runs behind the response, and the Location points at the operation to poll. New area-specific requests must appear in current provider coverage; discover it with GET /sms/numbers/area-codes?countryCode=US. Coverage and subscription checks run before operation creation. Billing follows delivery. Replays return the original operation without checking current coverage. The Idempotency-Key is required and permanent: repeating it returns the same operation forever, never a second number. A further number always needs a NEW key, including while others are still running. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.purchase_sms_number(input)).data + + async def release_imessage_dedicated_line( + self, input: ReleaseImessageDedicatedLineInput + ) -> models.Operation: + "Release dedicated iMessage line\n\nRemoves only iMessage from one dedicated line. Shared Voice is removed only when WhatsApp is absent; if WhatsApp remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open iMessage charge period identifies this product tenure." + return (await self._raw_resource.release_imessage_dedicated_line(input)).data + + async def release_resource(self, input: ReleaseResourceInput) -> models.Operation: + "Release resource\n\nGives one resource back, whatever it is. What that means is the resource's own business: an SMS number goes back to the provider and is retired, a dedicated iMessage line goes back to the shared pool and stays in existence for someone else to claim. Either way the provider is contacted first where there is one, then a single transaction disables every ability, ends the project's hold and closes the charge period — so a provider that refuses leaves the resource exactly as it was, still owned and still billed. Usually finishes inside this request and answers 200; if the provider is slow it answers 202 and the Location points at the operation to poll. The decrement runs behind the answer either way, so the resource is gone when you are told it is. Takes no Idempotency-Key — releasing the same resource twice is the same request. Releasing one that is already gone answers 404. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.release_resource(input)).data + + async def release_whatsapp_dedicated_line( + self, input: ReleaseWhatsappDedicatedLineInput + ) -> models.Operation: + "Release dedicated WhatsApp line\n\nRemoves only WhatsApp from one dedicated line. Shared Voice is removed only when iMessage is absent; if iMessage remains, Voice, the resource, ownership, and phone number are preserved. Usually finishes inside this request and answers 200; a slow workflow answers 202 with an operation to poll. Takes no Idempotency-Key because the open WhatsApp charge period identifies this product tenure." + return (await self._raw_resource.release_whatsapp_dedicated_line(input)).data + + async def release_whatsapp_shared_line_assignment( + self, input: ReleaseWhatsappSharedLineAssignmentInput + ) -> models.SharedLineAssignment: + "Release WhatsApp shared line assignment\n\nReleases a WhatsApp shared line assignment, freeing its seat for reassignment. The row is retained for audit and returned with releasedAt set, so repeating the call is safe. Requires the platforms:write permission bound to the project resource in the path." + return (await self._raw_resource.release_whatsapp_shared_line_assignment(input)).data + + async def replace_voice_profile_inbound( + self, input: ReplaceVoiceProfileInboundInput + ) -> models.VoiceProfileInboundConfiguration: + "Create or replace Voice profile inbound configuration\n\nCreates or fully replaces a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. Credentials are required and nullable; null removes destination authentication. Requires platforms:write bound to the path project." + return (await self._raw_resource.replace_voice_profile_inbound(input)).data + + async def rotate_voice_profile_outbound_credential( + self, input: RotateVoiceProfileOutboundCredentialInput + ) -> models.RotateVoiceProfileOutboundCredentialResponse: + "Rotate Voice outbound credential\n\nRotates a SIP profile's outbound credential when expectedVersion matches. The profileId may identify the default or an additional profile. Normal rotation gives the previous credential one hour of grace; emergency rotation gives none. The new password is returned once and is never recoverable. Requires platforms:write bound to the path project." + return (await self._raw_resource.rotate_voice_profile_outbound_credential(input)).data + + async def unassign_sms_line_campaign( + self, input: UnassignSmsLineCampaignInput + ) -> models.Operation: + "Remove SMS line campaign\n\nAny project writer, including a scoped API key, may detach the campaign. The number and campaign remain owned. Requires a permanent Idempotency-Key and expectedVersion. Local eligibility is blocked immediately; provider detachment runs asynchronously." + return (await self._raw_resource.unassign_sms_line_campaign(input)).data + + async def unassign_voice_line_profile(self, input: UnassignVoiceLineProfileInput) -> None: + "Unassign Voice line profile\n\nRemoves a line's explicit override when the resource version matches so the line follows the project default. Profiles and the pstn_voice ability are unchanged. Requires platforms:write bound to the path project." + return (await self._raw_resource.unassign_voice_line_profile(input)).data + + async def update_default_voice_profile( + self, input: UpdateDefaultVoiceProfileInput + ) -> models.VoiceProfile: + "Update default Voice profile\n\nPatches the default profile's protocol or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Its server-assigned name is immutable, and directional configuration uses the profileId returned by this resource. Requires platforms:write bound to the path project." + return (await self._raw_resource.update_default_voice_profile(input)).data + + async def update_voice_profile(self, input: UpdateVoiceProfileInput) -> models.VoiceProfile: + "Update Voice profile\n\nPatches an additional profile's name, protocol, or mediaEncryption when expectedVersion matches. Omitted fields are preserved. Directional configuration is managed through the profile's inbound and outbound endpoints. Requires platforms:write bound to the path project." + return (await self._raw_resource.update_voice_profile(input)).data + + async def update_voice_profile_inbound( + self, input: UpdateVoiceProfileInboundInput + ) -> models.VoiceProfileInboundConfiguration: + "Update Voice profile inbound configuration\n\nUpdates selected fields of a profile's inbound destination when the shared profile version matches. The profileId may identify the default or an additional profile. At least one of destinationUri or credentials is required. Credential omission preserves destination authentication, null removes it, and an object replaces it atomically. Requires platforms:write bound to the path project." + return (await self._raw_resource.update_voice_profile_inbound(input)).data + + async def update_voice_profile_outbound_authentication( + self, input: UpdateVoiceProfileOutboundAuthenticationInput + ) -> models.UpdateVoiceProfileOutboundAuthenticationResponse: + "Update Voice profile outbound authentication policy\n\nChanges a SIP profile's outbound Digest algorithm when expectedVersion matches. The profileId may identify the default or an additional profile. This policy-only change preserves the password, username, and any previous-password grace deadline. SHA-256 is recommended; MD5 is a weaker legacy option. Returns non-secret outbound metadata and the profile version. Requires platforms:write bound to the path project." + return (await self._raw_resource.update_voice_profile_outbound_authentication(input)).data + + +class AsyncProjectsAgentProfileResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsAgentProfileResource(transport) + + async def commit_avatar(self, input: CommitAgentProfileAvatarInput) -> models.AgentProfile: + "Commit an agent avatar\n\nCommits an agent avatar previously uploaded through createAgentProfileAvatarUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's Project ownership, size, content type, image bytes, dimensions, encryption, and age before changing the agent profile." + return (await self._raw_resource.commit_avatar(input)).data + + async def create_avatar_upload( + self, input: CreateAgentProfileAvatarUploadInput + ) -> models.AgentProfileAvatarUpload: + "Create an agent avatar upload\n\nCreates a ten-minute, Project-bound presigned S3 POST for a JPEG, PNG, or WebP agent avatar up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAgentProfileAvatar with the returned uploadId. Do not cache or log the upload URL or form fields." + return (await self._raw_resource.create_avatar_upload(input)).data + + async def get(self, input: GetAgentProfileInput) -> models.AgentProfile: + "Get an agent profile\n\nReturns the agent profile belonging to the identified project. The profile is project-scoped and is distinct from the authenticated account's personal profile. Use the dedicated avatar operations when uploading or removing an agent avatar." + return (await self._raw_resource.get(input)).data + + async def reset_avatar(self, input: ResetAgentProfileAvatarInput) -> models.AgentProfile: + "Reset an agent avatar\n\nReplaces the selected project's agent avatar with the project's default avatar, a generated planet image derived from the project ID, and returns the updated agent profile. The reset does not restore an earlier avatar: a custom avatar it replaces is discarded and must be uploaded and committed again to use it. When the default avatar is already in use, the profile is returned unchanged. This does not change the account's personal profile picture. Supply the required Idempotency-Key header." + return (await self._raw_resource.reset_avatar(input)).data + + async def update(self, input: UpdateAgentProfileInput) -> models.AgentProfile: + "Update an agent profile\n\nUpdates the supplied firstName and lastName fields in the project's agent profile and returns the updated profile. Avatar upload, commit and reset are separate operations. The caller must be authorized to change configuration for the selected project. Supply the required Idempotency-Key header." + return (await self._raw_resource.update(input)).data + + +class AsyncProjectsBillingResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsBillingResource(transport) + + async def get_operation(self, input: GetBillingOperationInput) -> models.BillingOperation: + "Get a billing operation snapshot\n\nReturns the authoritative state of a billing operation belonging to the selected project. Use it to recover or poll a plan-change request until the operation reaches success or failure. An accepted request is not evidence that the plan change has completed." + return (await self._raw_resource.get_operation(input)).data + + async def get_overview( + self, input: GetBillingOverviewInput + ) -> models.GetBillingOverviewResponse: + "Get the project's billing overview\n\nReturns the selected project's plan information, entitlements and current billing-period usage. This operation reads project billing state; it does not change plans or the payer's payment method. Organization-level plans are available through the organization billing overview." + return (await self._raw_resource.get_overview(input)).data + + async def list_billing_plans( + self, input: ListBillingPlansInput + ) -> models.ListBillingPlansResponse: + "List available billing plans\n\nLists the billing plan catalog, grouped by their public plan-metadata type. Use the returned plan information when choosing the category and planCode for a plan change. The catalog is the same for every project, and reading it does not purchase a plan." + return (await self._raw_resource.list_billing_plans(input)).data + + +class AsyncProjectsResource: + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawProjectsResource(transport) + self.agent_profile = AsyncProjectsAgentProfileResource(transport) + self.billing = AsyncProjectsBillingResource(transport) + self.platforms = AsyncProjectsPlatformsResource(transport) + + async def create_project_api_key( + self, input: CreateProjectApiKeyInput + ) -> models.CreateProjectApiKeyResponse: + "Create a project API key\n\nCreates a key bound to the selected project using the supplied name, permissions and optional expiry. The secret is returned only in this response and in idempotent replays of it; store it securely because other reads never return it. The key is scoped to this project and does not grant account-level access. Supply the required Idempotency-Key header." + return (await self._raw_resource.create_project_api_key(input)).data + + async def create_webhook_destination( + self, input: CreateWebhookDestinationInput + ) -> models.CreateWebhookDestinationResponse: + "Create a webhook destination\n\nCreates a webhook destination for the selected project using its URL, payload API version, event selection and other documented settings. The response includes the signing secret, which is returned only in this response and in idempotent replays of it, never by destination reads; store it securely for signature verification. The API version must be selectable and selected event types must belong to that version's catalog. Supply the required Idempotency-Key header." + return (await self._raw_resource.create_webhook_destination(input)).data + + async def delete(self, input: DeleteProjectInput) -> models.Project: + "Delete a project\n\nStarts deletion of the identified project using a credential authorized for project management. Inspect the documented response and use getProjectClosureStatus with the organization and project identifiers to read closure progress. A project API key is not an accepted credential for this operation." + return (await self._raw_resource.delete(input)).data + + async def delete_webhook_destination( + self, input: DeleteWebhookDestinationInput + ) -> models.WebhookDestination: + "Delete a webhook destination\n\nDeletes the selected project's destination and returns its stable tombstone. Repeated deletion returns the deletion representation. This operation removes the destination configuration; it is separate from disabling a destination through an update." + return (await self._raw_resource.delete_webhook_destination(input)).data + + async def download_attachment(self, input: DownloadAttachmentInput) -> bytes: + "Download an Attachment\n\nDownloads an Attachment's bytes. If unavailable after ten seconds, returns ATTACHMENT_NOT_READY with Retry-After: 5." + return (await self._raw_resource.download_attachment(input)).data + + async def get(self, input: GetProjectInput) -> models.Project: + "Get a project\n\nReturns the identified project's settings for an authorized caller. The credential must be allowed to access that project; possession of an unrelated project's key does not provide access. Missing and deleted projects are reported through the documented error responses." + return (await self._raw_resource.get(input)).data + + async def get_attachment(self, input: GetAttachmentInput) -> models.Attachment: + "Get an Attachment\n\nReturns an Attachment's metadata. Use the content endpoint to download its bytes." + return (await self._raw_resource.get_attachment(input)).data + + async def get_message_metrics_backfill( + self, input: GetMessageMetricsBackfillInput + ) -> models.GetMessageMetricsBackfillResponse: + "Get Metrics historical backfill status\n\nReturns historical metrics update progress. Completion reflects lastVerifiedAt; queries remain available during updates." + return (await self._raw_resource.get_message_metrics_backfill(input)).data + + async def get_message_metrics_sql_schema( + self, input: GetMessageMetricsSqlSchemaInput + ) -> models.GetMessageMetricsSqlSchemaResponse: + "Get messaging and voice metrics SQL schema\n\nReturns the message_events SQL schema, supported queries, and limits for the selected API version." + return (await self._raw_resource.get_message_metrics_sql_schema(input)).data + + async def get_webhook_destination( + self, input: GetWebhookDestinationInput + ) -> models.WebhookDestination: + "Get a webhook destination\n\nReturns the configuration of one webhook destination belonging to the selected project. Missing or deleted destinations are reported as errors. This read does not disclose the signing secret returned when the destination or a secret rotation was created." + return (await self._raw_resource.get_webhook_destination(input)).data + + async def get_webhook_event_schema( + self, input: GetWebhookEventSchemaInput + ) -> models.WebhookEventSchema | None: + "Get a webhook event schema\n\nReturns the published reader JSON Schema for eventType in the requested webhook apiVersion. Use it to interpret events for that exact payload version. The response media type is application/schema+json; an authorized conditional request may return 304 without a body. Unsupported event/version combinations are rejected." + return (await self._raw_resource.get_webhook_event_schema(input)).data + + async def list_attachments(self, input: ListAttachmentsInput) -> models.AttachmentPage: "List Project Attachments\n\nLists the Project's Attachment metadata, with optional time filters." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_ATTACHMENTS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_attachments(input)).data async def list_project_api_keys( self, input: ListProjectApiKeysInput - ) -> models.ListProjectApiKeysResponse | RawResponse[models.ListProjectApiKeysResponse]: + ) -> models.ListProjectApiKeysResponse: "List project API keys\n\nLists the API keys on the selected project, ordered newest first. Revoked keys are not listed; expired keys stay listed until they are revoked. Entries contain key metadata and permissions, never secret values. Use the returned identifiers to manage an existing key; lost secrets cannot be recovered through this operation." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_PROJECT_API_KEYS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_project_api_keys(input)).data async def list_webhook_api_versions( self, input: ListWebhookApiVersionsInput - ) -> ( - models.ListWebhookApiVersionsResponse - | None - | RawResponse[models.ListWebhookApiVersionsResponse | None] - ): + ) -> models.ListWebhookApiVersionsResponse | None: "List webhook API versions\n\nLists the published webhook payload API versions and their lifecycle metadata. The list is the same for every project. Use the selectable indicator when choosing a version for a destination. These payload dates are separate from SDK package versions. An authorized conditional request may return 304 without a response body." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WEBHOOK_API_VERSIONS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_webhook_api_versions(input)).data async def list_webhook_destinations( self, input: ListWebhookDestinationsInput - ) -> models.WebhookDestinationPage | RawResponse[models.WebhookDestinationPage]: + ) -> models.WebhookDestinationPage: "List webhook destinations\n\nReturns a cursor-paginated page of active webhook destinations configured for the selected project. Use pageSize and pageToken to navigate it. The listing returns destination configuration, never signing secrets." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WEBHOOK_DESTINATIONS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_webhook_destinations(input)).data async def list_webhook_egress_addresses( self, input: ListWebhookEgressAddressesInput - ) -> ( - models.ListWebhookEgressAddressesResponse - | None - | RawResponse[models.ListWebhookEgressAddressesResponse | None] - ): + ) -> models.ListWebhookEgressAddressesResponse | None: "List webhook egress addresses\n\nReturns the public network addresses from which this environment sends webhook deliveries. Use this information when configuring the receiving system's network allowlist. The result is environment-specific and does not describe the API service's ingress addresses." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WEBHOOK_EGRESS_ADDRESSES, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_webhook_egress_addresses(input)).data async def list_webhook_event_types( self, input: ListWebhookEventTypesInput - ) -> ( - models.ListWebhookEventTypesResponse - | None - | RawResponse[models.ListWebhookEventTypesResponse | None] - ): + ) -> models.ListWebhookEventTypesResponse | None: "List webhook event types\n\nLists the webhook event types available in the requested apiVersion, including their descriptions, audiences and reader-schema URLs. Use this versioned catalog when selecting a destination's enabledEvents. The response may include version-retirement information; an authorized conditional request can return 304 without a body." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_WEBHOOK_EVENT_TYPES, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_webhook_event_types(input)).data async def query_message_metrics( self, input: QueryMessageMetricsInput - ) -> models.QueryMessageMetricsResponse | RawResponse[models.QueryMessageMetricsResponse]: + ) -> models.QueryMessageMetricsResponse: "Query messaging and voice metrics with SQL\n\nRuns read-only SQL over the Project's message_events table. Get the SQL schema for supported columns, capabilities, and limits." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_QUERY_MESSAGE_METRICS, payload) - return response if self._raw else response.data + return (await self._raw_resource.query_message_metrics(input)).data async def revoke_project_api_key( self, input: RevokeProjectApiKeyInput - ) -> models.ProjectApiKeyResponse | RawResponse[models.ProjectApiKeyResponse]: + ) -> models.ProjectApiKeyResponse: "Delete a project API key\n\nRevokes the identified key on the selected project and returns its revoked metadata. Repeating the deletion returns the same revokedAt value. This operation does not rotate the key or return a replacement secret. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REVOKE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return (await self._raw_resource.revoke_project_api_key(input)).data async def rotate_webhook_signing_secret( self, input: RotateWebhookSigningSecretInput - ) -> ( - models.RotateWebhookSigningSecretResponse - | RawResponse[models.RotateWebhookSigningSecretResponse] - ): + ) -> models.RotateWebhookSigningSecretResponse: "Rotate a webhook signing secret\n\nRotates the signing secret for the selected project's webhook destination and returns the new secret. The optional overlapSeconds controls the requested overlap with the previous secret according to the documented request constraints. Store the new secret securely and update the receiver's signature verification configuration; it is returned only in this response and in idempotent replays of it, never by destination reads. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_ROTATE_WEBHOOK_SIGNING_SECRET, payload) - return response if self._raw else response.data + return (await self._raw_resource.rotate_webhook_signing_secret(input)).data - async def update( - self, input: UpdateProjectInput - ) -> models.Project | RawResponse[models.Project]: + async def update(self, input: UpdateProjectInput) -> models.Project: "Update a project\n\nUpdates the identified project's name and returns the updated project. The project slug is not a mutable field in this request. Use an authorized account or organization service-identity credential; a project API key is not accepted. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_PROJECT, payload) - return response if self._raw else response.data + return (await self._raw_resource.update(input)).data async def update_project_api_key( self, input: UpdateProjectApiKeyInput - ) -> models.ProjectApiKeyResponse | RawResponse[models.ProjectApiKeyResponse]: + ) -> models.ProjectApiKeyResponse: "Update a project API key's permissions\n\nReplaces the identified project key's permission list with the supplied permissions and returns the updated metadata. Sending the permission list the key already has leaves it unchanged. This request does not create a new secret or change the key's project binding. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_PROJECT_API_KEY, payload) - return response if self._raw else response.data + return (await self._raw_resource.update_project_api_key(input)).data async def update_webhook_destination( self, input: UpdateWebhookDestinationInput - ) -> models.WebhookDestination | RawResponse[models.WebhookDestination]: + ) -> models.WebhookDestination: "Update a webhook destination\n\nUpdates the supplied URL, name, description, status or enabledEvents fields on a project's webhook destination and returns its updated configuration. The payload API version is not a mutable field in this request. Event selections are checked against the destination's versioned catalog; signing-secret rotation is a separate operation. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_WEBHOOK_DESTINATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.update_webhook_destination(input)).data - async def upload_attachment( - self, input: UploadAttachmentInput - ) -> models.Attachment | RawResponse[models.Attachment]: + async def upload_attachment(self, input: UploadAttachmentInput) -> models.Attachment: "Upload an Attachment\n\nUploads a file and returns its Attachment once ready to download." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True, exclude={"body"}) - payload["body"] = input.body.root if input.body is not None else None - response = await self._transport.request(_OP_UPLOAD_ATTACHMENT, payload) - return response if self._raw else response.data + return (await self._raw_resource.upload_attachment(input)).data class AsyncAuthDeviceResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawAuthDeviceResource(transport) async def authorize( self, input: DeviceAuthorizeInput | None = None - ) -> models.DeviceAuthorizeResponse | RawResponse[models.DeviceAuthorizeResponse]: + ) -> models.DeviceAuthorizeResponse: "Start Device Authorization\n\nStarts the device authorization flow for a CLI or another device without a browser. Show the verification URL and user code, then poll the token endpoint at the returned interval. No request fields are required; any supplied body is ignored." - payload = (input or DeviceAuthorizeInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_DEVICE_AUTHORIZE, payload) - return response if self._raw else response.data + return (await self._raw_resource.authorize(input)).data - async def token( - self, input: DeviceTokenInput - ) -> models.DeviceTokenResponse | RawResponse[models.DeviceTokenResponse]: + async def token(self, input: DeviceTokenInput) -> models.DeviceTokenResponse: "Exchange Device Code or Refresh Token\n\nExchanges an authorized device code or a refresh token for an access token and rotating refresh token. Accepts JSON and form-encoded bodies. While polling, wait at least interval seconds and increase the interval on slow_down. Store the new refresh token after every successful grant.\n\nThis SDK method sends uncompressed JSON (application/json). Other request formats described above apply to direct HTTP requests." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DEVICE_TOKEN, payload) - return response if self._raw else response.data + return (await self._raw_resource.token(input)).data class AsyncAuthResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw - self.device = AsyncAuthDeviceResource(transport, raw) + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawAuthResource(transport) + self.device = AsyncAuthDeviceResource(transport) async def begin_invitation_sso( self, input: BeginInvitationSsoInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> models.OrganizationAuthenticationRedirect: "Authenticate to an invitation's organization SSO connection\n\nReturns an authentication URL for the organization SSO connection associated with the supplied invitation token. Supply token and returnTo. Complete the returned authentication flow; requesting its URL does not itself accept the invitation." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_BEGIN_INVITATION_SSO, payload) - return response if self._raw else response.data + return (await self._raw_resource.begin_invitation_sso(input)).data async def begin_organization_authentication( self, input: BeginOrganizationAuthenticationInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> models.OrganizationAuthenticationRedirect: "Authenticate to the current organization SSO connection\n\nReturns a URL to authenticate through the selected organization’s current SSO connection. Supply returnTo and open the returned URL to continue the flow. Receiving the URL does not establish an authenticated session." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_BEGIN_ORGANIZATION_AUTHENTICATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.begin_organization_authentication(input)).data async def begin_organization_closure_authentication( self, input: BeginOrganizationClosureAuthenticationInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> models.OrganizationAuthenticationRedirect: "Authenticate the current Owner to inspect organization closure\n\nReturns an authentication URL for the current organization owner to inspect organization closure. Supply returnTo and complete the returned flow. This operation initiates authentication and does not close the organization." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_BEGIN_ORGANIZATION_CLOSURE_AUTHENTICATION, payload - ) - return response if self._raw else response.data + return (await self._raw_resource.begin_organization_closure_authentication(input)).data async def begin_organization_sso_admission( self, input: BeginOrganizationSsoAdmissionInput - ) -> ( - models.OrganizationAuthenticationRedirect - | RawResponse[models.OrganizationAuthenticationRedirect] - ): + ) -> models.OrganizationAuthenticationRedirect: "Begin organization SSO admission for an existing Account\n\nReturns an SSO admission URL for an existing account and the selected organization. Supply returnTo for the continuation URL. Admission requires completing the returned authentication flow; creating the URL does not itself grant membership." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_BEGIN_ORGANIZATION_SSO_ADMISSION, payload) - return response if self._raw else response.data + return (await self._raw_resource.begin_organization_sso_admission(input)).data async def create_organization_sso_portal_link( self, input: CreateOrganizationSsoPortalLinkInput - ) -> ( - models.CreateOrganizationSsoPortalLinkResponse - | RawResponse[models.CreateOrganizationSsoPortalLinkResponse] - ): + ) -> models.CreateOrganizationSsoPortalLinkResponse: "Create organization SSO setup portal\n\nReturns an organization setup portal URL. Supply returnTo and optionally intent, either sso or domain_verification; sso is the default. Open the returned URL to complete the selected setup flow." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_ORGANIZATION_SSO_PORTAL_LINK, payload) - return response if self._raw else response.data + return (await self._raw_resource.create_organization_sso_portal_link(input)).data async def disable_organization_sso( self, input: DisableOrganizationSsoInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> models.OrganizationSsoConfiguration: "Turn organization SSO off\n\nDeletes the provider connection, releases the SSO requirement once the connection is gone, then unbinds the chosen domains. Retry with the same Idempotency-Key to resume or await the same run." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_DISABLE_ORGANIZATION_SSO, payload) - return response if self._raw else response.data + return (await self._raw_resource.disable_organization_sso(input)).data async def get_organization_connection_status( self, input: GetOrganizationConnectionStatusInput - ) -> models.OrganizationConnectionStatus | RawResponse[models.OrganizationConnectionStatus]: + ) -> models.OrganizationConnectionStatus: "Read organization and own membership synchronization\n\nRequires current human organization membership. Synchronization status does not attest SSO configuration or completed authorization." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_ORGANIZATION_CONNECTION_STATUS, payload) - return response if self._raw else response.data + return (await self._raw_resource.get_organization_connection_status(input)).data async def get_organization_sso_configuration( self, input: GetOrganizationSsoConfigurationInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> models.OrganizationSsoConfiguration: "Read organization SSO configuration\n\nReturns the selected organization’s SSO connection state, configuration version, and desired and effective policy settings. Read policySyncStatus alongside the enforcement fields to distinguish requested settings from synchronized settings." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_ORGANIZATION_SSO_CONFIGURATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.get_organization_sso_configuration(input)).data async def list_oauth_scopes( self, input: ListOauthScopesInput | None = None - ) -> models.ListOauthScopesResponse | RawResponse[models.ListOauthScopesResponse]: + ) -> models.ListOauthScopesResponse: "List OAuth scopes\n\nLists the business permissions available to OAuth applications." - payload = (input or ListOauthScopesInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_LIST_OAUTH_SCOPES, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_oauth_scopes(input)).data async def refresh_organization_sso_connection( self, input: RefreshOrganizationSsoConnectionInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> models.OrganizationSsoConfiguration: "Refresh organization SSO connection\n\nRefreshes the selected organization’s SSO connection and returns its current connection state, configuration version and policy synchronization status. This operation takes no request body." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REFRESH_ORGANIZATION_SSO_CONNECTION, payload) - return response if self._raw else response.data + return (await self._raw_resource.refresh_organization_sso_connection(input)).data async def retry_organization_connection_sync( self, input: RetryOrganizationConnectionSyncInput - ) -> models.OrganizationConnectionStatus | RawResponse[models.OrganizationConnectionStatus]: + ) -> models.OrganizationConnectionStatus: "Retry own organization connection synchronization\n\nReconciles existing local intent. Takes no body and cannot change membership, roles or authentication policy." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RETRY_ORGANIZATION_CONNECTION_SYNC, payload) - return response if self._raw else response.data + return (await self._raw_resource.retry_organization_connection_sync(input)).data async def start_enterprise_login( self, input: StartEnterpriseLoginInput - ) -> models.StartEnterpriseLoginResponse | RawResponse[models.StartEnterpriseLoginResponse]: + ) -> models.StartEnterpriseLoginResponse: "Start company sign-in without an existing Account\n\nReturns a sign-in URL without requiring an existing account: the company SSO connection when the target has a ready connection, otherwise ordinary account login. Supply one documented enrollment variant: organizationId with returnTo (optionally invitationToken), invitationToken with returnTo, or retryToken. Open the returned URL to continue authentication; receiving a URL does not complete sign-in. This is a browser flow: the request must come from an allowed Origin, and the retryToken variant also needs the retry cookie set by the failed sign-in, so send it with credentials." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_START_ENTERPRISE_LOGIN, payload) - return response if self._raw else response.data + return (await self._raw_resource.start_enterprise_login(input)).data async def update_organization_sso_policy( self, input: UpdateOrganizationSsoPolicyInput - ) -> models.OrganizationSsoConfiguration | RawResponse[models.OrganizationSsoConfiguration]: + ) -> models.OrganizationSsoConfiguration: "Update organization SSO policy\n\nUpdates whether SSO can admit new members automatically using ssoJitEnabled and the current expectedVersion. Returns the organization’s SSO configuration and policy synchronization status; a successful response does not mean every desired policy setting has finished synchronizing." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_ORGANIZATION_SSO_POLICY, payload) - return response if self._raw else response.data + return (await self._raw_resource.update_organization_sso_policy(input)).data class AsyncOrganizationsBillingResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawOrganizationsBillingResource(transport) async def cancel_subscription( self, input: CancelSubscriptionInput - ) -> models.CancelSubscriptionResponse | RawResponse[models.CancelSubscriptionResponse]: + ) -> models.CancelSubscriptionResponse: "Cancel a category at the end of its billing period\n\nSchedules cancellation of the specified project's billing category at the end of its current period. The category remains active through the returned cancelsAt instant and then stops renewing. This is a scheduled cancellation, not an immediate removal of the remaining period's service. If the category has no active subscription, nothing changes and the response has cancellationScheduled set to false and cancelsAt set to null. Supply both organizationId and projectId to select the project within its organization." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CANCEL_SUBSCRIPTION, payload) - return response if self._raw else response.data + return (await self._raw_resource.cancel_subscription(input)).data async def change_plan( self, input: ChangePlanInput - ) -> ( - models.TerminalBillingOperation - | models.PendingBillingOperation - | RawResponse[models.TerminalBillingOperation | models.PendingBillingOperation] - ): + ) -> models.TerminalBillingOperation | models.PendingBillingOperation: "Purchase or change a category's plan\n\nPurchases or changes the selected project's plan for the supplied category and planCode. A 202 response means the change is pending: poll the returned operation URL and honor Retry-After until it succeeds or fails. A 200 response means the idempotency key resolved to an operation that is already terminal; inspect that result rather than assuming success from the status code alone. Supply the required Idempotency-Key header. Supply both organizationId and projectId to select the project within its organization." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CHANGE_PLAN, payload) - return response if self._raw else response.data + return (await self._raw_resource.change_plan(input)).data async def create_organization_payment_method_checkout( self, input: CreateOrganizationPaymentMethodCheckoutInput - ) -> ( - models.CreateOrganizationPaymentMethodCheckoutResponse - | RawResponse[models.CreateOrganizationPaymentMethodCheckoutResponse] - ): + ) -> models.CreateOrganizationPaymentMethodCheckoutResponse: "Get a payment-method checkout URL for the organization\n\nReturns a hosted payment-method collection URL for the selected organization. An Idempotency-Key header is optional; supply one to make retries safe. Complete the returned checkout flow. Receiving the URL does not mean a card has been saved; check payment-method status afterward." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request( - _OP_CREATE_ORGANIZATION_PAYMENT_METHOD_CHECKOUT, payload - ) - return response if self._raw else response.data + return (await self._raw_resource.create_organization_payment_method_checkout(input)).data async def create_organization_setup_intent( self, input: CreateOrganizationSetupIntentInput - ) -> ( - models.CreateOrganizationSetupIntentResponse - | RawResponse[models.CreateOrganizationSetupIntentResponse] - ): + ) -> models.CreateOrganizationSetupIntentResponse: "Create a SetupIntent for an in-app card capture\n\nCreates payment-provider configuration for collecting a card for the selected organization and returns clientSecret and publishableKey. Supply the required Idempotency-Key header. Complete the provider’s card-collection flow separately and avoid logging the returned client secret." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_ORGANIZATION_SETUP_INTENT, payload) - return response if self._raw else response.data + return (await self._raw_resource.create_organization_setup_intent(input)).data async def get_organization_billing_overview( self, input: GetOrganizationBillingOverviewInput - ) -> ( - models.GetOrganizationBillingOverviewResponse - | RawResponse[models.GetOrganizationBillingOverviewResponse] - ): + ) -> models.GetOrganizationBillingOverviewResponse: "Get the organization's billing overview\n\nReturns the selected organization’s billing subscription and entitlementsVersion. The subscription can be null. Read the returned plan, charges and entitlements to inspect organization billing; this operation does not purchase or change a plan." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_ORGANIZATION_BILLING_OVERVIEW, payload) - return response if self._raw else response.data + return (await self._raw_resource.get_organization_billing_overview(input)).data async def get_organization_payment_method( self, input: GetOrganizationPaymentMethodInput - ) -> ( - models.GetOrganizationPaymentMethodResponse - | RawResponse[models.GetOrganizationPaymentMethodResponse] - ): + ) -> models.GetOrganizationPaymentMethodResponse: "Check the organization for a card on file\n\nReports whether the selected organization has a card on file and returns its documented payment-method metadata. Reading this endpoint does not collect a new card or create a checkout session." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_ORGANIZATION_PAYMENT_METHOD, payload) - return response if self._raw else response.data + return (await self._raw_resource.get_organization_payment_method(input)).data - async def list_invoices( - self, input: ListInvoicesInput - ) -> models.ListInvoicesResponse | RawResponse[models.ListInvoicesResponse]: + async def list_invoices(self, input: ListInvoicesInput) -> models.ListInvoicesResponse: "List invoices\n\nReturns a single page of the selected organization's invoices; invoices with a zero total are excluded. Use the documented invoice fields to inspect each invoice's billing state. Listing invoices does not make a payment or modify a subscription." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_INVOICES, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_invoices(input)).data async def resume_subscription( self, input: ResumeSubscriptionInput - ) -> models.ResumeSubscriptionResponse | RawResponse[models.ResumeSubscriptionResponse]: + ) -> models.ResumeSubscriptionResponse: "Resume a category scheduled for cancellation\n\nRemoves a scheduled cancellation for the specified billing category on the selected project so it can renew normally. This operation resumes a category scheduled to cancel; it is separate from purchasing or changing a plan. Supply both organizationId and projectId to select the project within its organization." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RESUME_SUBSCRIPTION, payload) - return response if self._raw else response.data + return (await self._raw_resource.resume_subscription(input)).data class AsyncOrganizationsProjectsResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawOrganizationsProjectsResource(transport) async def check_project_slug_availability( self, input: CheckProjectSlugAvailabilityInput - ) -> ( - models.CheckProjectSlugAvailabilityResponse - | RawResponse[models.CheckProjectSlugAvailabilityResponse] - ): + ) -> models.CheckProjectSlugAvailabilityResponse: "Check slug availability\n\nReports whether createProject would accept `slug` right now. Advisory: only the create itself allocates, so a caller must still handle SLUG_TAKEN. A malformed slug is rejected on shape; a reserved slug, a slug held by an active project, and a slug retired with a deleted project each answer `available: false` with a reason." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CHECK_PROJECT_SLUG_AVAILABILITY, payload) - return response if self._raw else response.data + return (await self._raw_resource.check_project_slug_availability(input)).data - async def count( - self, input: CountProjectsInput - ) -> models.ProjectCount | RawResponse[models.ProjectCount]: + async def count(self, input: CountProjectsInput) -> models.ProjectCount: "Count accessible projects\n\nCounts the projects the same filter would list. The count is read from the primary, so it is authoritative rather than replica-lagged." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_COUNT_PROJECTS, payload) - return response if self._raw else response.data + return (await self._raw_resource.count(input)).data - async def create( - self, input: CreateProjectInput - ) -> models.Project | RawResponse[models.Project]: + async def create(self, input: CreateProjectInput) -> models.Project: "Create a project\n\nCreates in the authorized organization. In addition to account credentials, explicitly granted Service Identity API keys and M2M tokens may create projects. Project API keys cannot create projects. Creator and private credential evidence come only from the trusted authorization context. The caller-selected slug is immutable, must be 3 to 63 lowercase ASCII alphanumerics separated by single hyphens, and cannot be reserved or held by any active or deleted project." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_PROJECT, payload) - return response if self._raw else response.data + return (await self._raw_resource.create(input)).data async def get_project_closure_status( self, input: GetProjectClosureStatusInput - ) -> ( - models.GetProjectClosureStatusResponse | RawResponse[models.GetProjectClosureStatusResponse] - ): + ) -> models.GetProjectClosureStatusResponse: "Read project closure progress\n\nReturns closure progress for projectId within organizationId, including deletionOperationId, domain progress and ready. This read operation does not initiate deletion." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_GET_PROJECT_CLOSURE_STATUS, payload) - return response if self._raw else response.data + return (await self._raw_resource.get_project_closure_status(input)).data - async def list( - self, input: ListProjectsInput - ) -> models.ProjectPage | RawResponse[models.ProjectPage]: + async def list(self, input: ListProjectsInput) -> models.ProjectPage: "List accessible projects\n\nReturns a cursor-paginated page of the active projects in organizationId. Filter using query and the documented creation-time bounds, and navigate with pageSize and pageToken. Project roles are not returned and role is not a supported filter." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_LIST_PROJECTS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list(input)).data class AsyncOrganizationsResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw - self.billing = AsyncOrganizationsBillingResource(transport, raw) - self.projects = AsyncOrganizationsProjectsResource(transport, raw) + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawOrganizationsResource(transport) + self.billing = AsyncOrganizationsBillingResource(transport) + self.projects = AsyncOrganizationsProjectsResource(transport) class AsyncAccountResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawAccountResource(transport) async def commit_profile_picture( self, input: CommitAccountProfilePictureInput - ) -> models.Account | RawResponse[models.Account]: + ) -> models.Account: "Commit a profile picture\n\nCommits a profile picture previously uploaded through createAccountProfilePictureUpload. Call this only after the direct multipart upload succeeds, using the uploadId from the same upload session and a stable Idempotency-Key. The service validates the temporary object's ownership, size, content type, image bytes, dimensions, encryption, and age before changing the Account." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_COMMIT_ACCOUNT_PROFILE_PICTURE, payload) - return response if self._raw else response.data + return (await self._raw_resource.commit_profile_picture(input)).data async def confirm_phone_verification( self, input: ConfirmAccountPhoneVerificationInput - ) -> models.Account | RawResponse[models.Account]: + ) -> models.Account: "Confirm a phone number verification\n\nBinds the number once the code is approved. Repeat calls return the bound Account." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CONFIRM_ACCOUNT_PHONE_VERIFICATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.confirm_phone_verification(input)).data async def create_account_service_key( self, input: CreateAccountServiceKeyInput - ) -> ( - models.CreateAccountServiceKeyResponse | RawResponse[models.CreateAccountServiceKeyResponse] - ): + ) -> models.CreateAccountServiceKeyResponse: "Create an Account Service Key\n\nCreates a service key for the authenticated account with the supplied name and optional expiresAt. Returns key metadata and a one-time credential; store the credential securely because it cannot be retrieved through the listing endpoint. These credentials act as the account and must not be distributed as project-scoped keys. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_ACCOUNT_SERVICE_KEY, payload) - return response if self._raw else response.data + return (await self._raw_resource.create_account_service_key(input)).data async def create_profile_picture_upload( self, input: CreateAccountProfilePictureUploadInput - ) -> models.ProfilePictureUpload | RawResponse[models.ProfilePictureUpload]: + ) -> models.ProfilePictureUpload: "Create a profile picture upload\n\nCreates a ten-minute, Account-bound presigned S3 POST for a JPEG, PNG, or WebP profile picture up to 5 MiB. Copy every returned formFields entry into a multipart/form-data request to uploadUrl, append the local file as the final form part, and upload it directly without sending Photon credentials. After the upload succeeds, call commitAccountProfilePicture with the returned uploadId. Do not cache or log the upload URL or form fields." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_ACCOUNT_PROFILE_PICTURE_UPLOAD, payload) - return response if self._raw else response.data + return (await self._raw_resource.create_profile_picture_upload(input)).data - async def delete( - self, input: DeleteAccountInput | None = None - ) -> models.Account | RawResponse[models.Account]: + async def delete(self, input: DeleteAccountInput | None = None) -> models.Account: "Delete the authenticated account\n\nDeletes the authenticated account and returns its account tombstone. The operation is rejected while the account still owns organizations; transfer or close those organizations before retrying. This endpoint acts on the caller's account and does not accept another account's identifier." - payload = (input or DeleteAccountInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_DELETE_ACCOUNT, payload) - return response if self._raw else response.data + return (await self._raw_resource.delete(input)).data - async def get( - self, input: GetAccountInput | None = None - ) -> models.Account | RawResponse[models.Account]: + async def get(self, input: GetAccountInput | None = None) -> models.Account: "Get the authenticated account\n\nReturns the profile of the authenticated account. The account is selected from the credential rather than a request parameter. A missing or deleted account is reported as an error instead of an empty profile." - payload = (input or GetAccountInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_GET_ACCOUNT, payload) - return response if self._raw else response.data + return (await self._raw_resource.get(input)).data async def list_account_service_keys( self, input: ListAccountServiceKeysInput | None = None - ) -> models.ListAccountServiceKeysResponse | RawResponse[models.ListAccountServiceKeysResponse]: + ) -> models.ListAccountServiceKeysResponse: "List Account Service Keys\n\nReturns metadata for the authenticated account's unrevoked service keys, including expired keys, ordered newest first. Secret values are not returned; a key's credential is disclosed only when that key is created." - payload = (input or ListAccountServiceKeysInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_LIST_ACCOUNT_SERVICE_KEYS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_account_service_keys(input)).data async def list_authorized_applications( self, input: ListAuthorizedApplicationsInput | None = None - ) -> ( - models.ListAuthorizedApplicationsResponse - | RawResponse[models.ListAuthorizedApplicationsResponse] - ): + ) -> models.ListAuthorizedApplicationsResponse: "List connected applications\n\nLists the OAuth applications authorized by the authenticated user." - payload = (input or ListAuthorizedApplicationsInput()).model_dump( - mode="json", by_alias=True, exclude_unset=True - ) - response = await self._transport.request(_OP_LIST_AUTHORIZED_APPLICATIONS, payload) - return response if self._raw else response.data + return (await self._raw_resource.list_authorized_applications(input)).data - async def reset_profile_picture( - self, input: ResetAccountProfilePictureInput - ) -> models.Account | RawResponse[models.Account]: + async def reset_profile_picture(self, input: ResetAccountProfilePictureInput) -> models.Account: "Remove a profile picture\n\nRemoves the authenticated account's custom profile picture and returns the account using its default picture. This operation does not upload a replacement; use the upload-and-commit operations when setting a new custom picture. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_RESET_ACCOUNT_PROFILE_PICTURE, payload) - return response if self._raw else response.data + return (await self._raw_resource.reset_profile_picture(input)).data async def revoke_account_service_key( self, input: RevokeAccountServiceKeyInput - ) -> ( - models.RevokeAccountServiceKeyResponse | RawResponse[models.RevokeAccountServiceKeyResponse] - ): + ) -> models.RevokeAccountServiceKeyResponse: "Revoke an Account Service Key\n\nRevokes the account-owned service key identified by serviceKeyId and returns its revoked metadata. Repeating the revocation is stable. Revocation changes the credential's validity; it does not create a replacement key. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REVOKE_ACCOUNT_SERVICE_KEY, payload) - return response if self._raw else response.data + return (await self._raw_resource.revoke_account_service_key(input)).data - async def revoke_authorized_application( - self, input: RevokeAuthorizedApplicationInput - ) -> None | RawResponse[None]: + async def revoke_authorized_application(self, input: RevokeAuthorizedApplicationInput) -> None: "Revoke a connected application\n\nRevokes the authenticated user's grant for one OAuth application." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REVOKE_AUTHORIZED_APPLICATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.revoke_authorized_application(input)).data async def start_phone_verification( self, input: StartAccountPhoneVerificationInput - ) -> ( - models.StartAccountPhoneVerificationResponse - | RawResponse[models.StartAccountPhoneVerificationResponse] - ): + ) -> models.StartAccountPhoneVerificationResponse: "Start a phone number verification\n\nSends an SMS code. Answers CAPTCHA_REQUIRED with the widget to render when no solved challenge accompanies the request; retry with the returned challengeContext and a token. Rate limited per account, per destination number, and globally; a rejection carries Retry-After." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_START_ACCOUNT_PHONE_VERIFICATION, payload) - return response if self._raw else response.data + return (await self._raw_resource.start_phone_verification(input)).data - async def update( - self, input: UpdateAccountInput - ) -> models.Account | RawResponse[models.Account]: + async def update(self, input: UpdateAccountInput) -> models.Account: "Update the authenticated account\n\nUpdates the supplied firstName and lastName fields on the authenticated account and returns the updated profile. Only the documented profile fields can be changed through this endpoint; profile-picture uploads and phone-number verification use their dedicated operations. Supply the required Idempotency-Key header." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_UPDATE_ACCOUNT, payload) - return response if self._raw else response.data + return (await self._raw_resource.update(input)).data class AsyncSystemResource: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self._transport = transport - self._raw = raw + def __init__(self, transport: AsyncTransport) -> None: + self._raw_resource = AsyncRawSystemResource(transport) async def create_app_installation_request( self, input: CreateAppInstallationRequestInput - ) -> ( - models.CreateAppInstallationRequestResponse - | RawResponse[models.CreateAppInstallationRequestResponse] - ): + ) -> models.CreateAppInstallationRequestResponse: "Request an app installation\n\nAuthenticates a registered app backend using a short-lived signed client assertion. Creates request metadata only; customer approval is still required." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_CREATE_APP_INSTALLATION_REQUEST, payload) - return response if self._raw else response.data + return (await self._raw_resource.create_app_installation_request(input)).data async def redeem_app_installation_delivery( self, input: RedeemAppInstallationDeliveryInput - ) -> ( - models.RedeemAppInstallationDeliveryResponse - | RawResponse[models.RedeemAppInstallationDeliveryResponse] - ): + ) -> models.RedeemAppInstallationDeliveryResponse: "Redeem an approved installation credential\n\nThe registered app backend authenticates with a signed client assertion and a single-use code. Plaintext is returned only once; retries return status and never create another credential." - payload = input.model_dump(mode="json", by_alias=True, exclude_unset=True) - response = await self._transport.request(_OP_REDEEM_APP_INSTALLATION_DELIVERY, payload) - return response if self._raw else response.data + return (await self._raw_resource.redeem_app_installation_delivery(input)).data + + +class SyncRawRoot: + def __init__(self, transport: SyncTransport) -> None: + self.account = SyncRawAccountResource(transport) + self.auth = SyncRawAuthResource(transport) + self.organizations = SyncRawOrganizationsResource(transport) + self.projects = SyncRawProjectsResource(transport) + self.system = SyncRawSystemResource(transport) class SyncRoot: - def __init__(self, transport: SyncTransport, raw: bool = False) -> None: - self.account = SyncAccountResource(transport, raw) - self.auth = SyncAuthResource(transport, raw) - self.organizations = SyncOrganizationsResource(transport, raw) - self.projects = SyncProjectsResource(transport, raw) - self.system = SyncSystemResource(transport, raw) + def __init__(self, transport: SyncTransport) -> None: + self.account = SyncAccountResource(transport) + self.auth = SyncAuthResource(transport) + self.organizations = SyncOrganizationsResource(transport) + self.projects = SyncProjectsResource(transport) + self.system = SyncSystemResource(transport) + + +class AsyncRawRoot: + def __init__(self, transport: AsyncTransport) -> None: + self.account = AsyncRawAccountResource(transport) + self.auth = AsyncRawAuthResource(transport) + self.organizations = AsyncRawOrganizationsResource(transport) + self.projects = AsyncRawProjectsResource(transport) + self.system = AsyncRawSystemResource(transport) class AsyncRoot: - def __init__(self, transport: AsyncTransport, raw: bool = False) -> None: - self.account = AsyncAccountResource(transport, raw) - self.auth = AsyncAuthResource(transport, raw) - self.organizations = AsyncOrganizationsResource(transport, raw) - self.projects = AsyncProjectsResource(transport, raw) - self.system = AsyncSystemResource(transport, raw) + def __init__(self, transport: AsyncTransport) -> None: + self.account = AsyncAccountResource(transport) + self.auth = AsyncAuthResource(transport) + self.organizations = AsyncOrganizationsResource(transport) + self.projects = AsyncProjectsResource(transport) + self.system = AsyncSystemResource(transport) diff --git a/packages/python/tests/test_client.py b/packages/python/tests/test_client.py index 0cc78e0..0c138b0 100644 --- a/packages/python/tests/test_client.py +++ b/packages/python/tests/test_client.py @@ -187,9 +187,10 @@ def check(photon: Photon | AsyncPhoton) -> None: if name.startswith("_"): continue resource = getattr(photon, name) - assert type(resource) is type(raw_resource) - assert resource._raw is False - assert raw_resource._raw is True + # The client's resource returns decoded results through its raw + # counterpart, which returns whole responses (photon.raw). + assert type(resource._raw_resource) is type(raw_resource) + assert type(resource) is not type(raw_resource) if asynchronous: diff --git a/packages/python/tests/typing_check.py b/packages/python/tests/typing_check.py new file mode 100644 index 0000000..f4592af --- /dev/null +++ b/packages/python/tests/typing_check.py @@ -0,0 +1,24 @@ +"""Static typing check, run by pyright in CI (not a pytest module). + +The client's methods return decoded results; photon.raw returns whole responses. +""" + +from typing import assert_type + +from photon_api import AsyncPhoton, Photon +from photon_api.generated import models +from photon_api.rpc_generated import ListProjectsInput +from photon_api.transport import RawResponse + + +def sync_usage(photon: Photon) -> None: + assert_type(photon.account.get(), models.Account) + assert_type(photon.raw.account.get(), RawResponse[models.Account]) + request = ListProjectsInput.model_validate({"path": {"organizationId": "o"}}) + assert_type(photon.organizations.projects.list(request), models.ProjectPage) + assert_type(photon.auth.list_oauth_scopes().scopes, list[str]) + + +async def async_usage(photon: AsyncPhoton) -> None: + assert_type(await photon.account.get(), models.Account) + assert_type(await photon.raw.account.get(), RawResponse[models.Account]) diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index bf19388..61123ce 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "photonhq-api" -version = "0.2.0" +version = "0.2.1" description = "Spargen-generated Photon OpenAPI 3.1 client" edition.workspace = true license.workspace = true diff --git a/packages/typescript/package.json b/packages/typescript/package.json index a587e5c..1728fdc 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@photon-ai/api", - "version": "0.2.0", + "version": "0.2.1", "description": "Generated Photon API RPC client for Node.js and browsers", "license": "MIT", "type": "module", diff --git a/tools/openapi/src/generate-facades.test.ts b/tools/openapi/src/generate-facades.test.ts index bc1413a..ade930c 100644 --- a/tools/openapi/src/generate-facades.test.ts +++ b/tools/openapi/src/generate-facades.test.ts @@ -87,10 +87,11 @@ test("all convenience methods retain summaries and descriptions safely", () => { const docstrings = [...python.matchAll( /^ (?:async )?def get_example\([^\n]+\n([^\n]+)/gm, )]; - assert.deepEqual(docstrings.map((match) => JSON.parse(match[1]!.trim())), [ - `${documented.summary}\n\n${documented.description}`, - `${documented.summary}\n\n${documented.description}`, - ]); + // Sync and async, each in the client and its raw counterpart (photon.raw). + assert.deepEqual( + docstrings.map((match) => JSON.parse(match[1]!.trim())), + Array(4).fill(`${documented.summary}\n\n${documented.description}`), + ); }); test("legacy manifests without documentation still generate valid method bodies", () => { @@ -137,7 +138,8 @@ test("Python binary methods expose bytes while retaining empty response handling })] }); assert.match(source, /"200": bytes/); assert.match(source, /"204": None/); - assert.match(source, /bytes \| None \| RawResponse\[bytes \| None\]/); + assert.match(source, /def download\(self[^\n]*\) -> bytes \| None:/); + assert.match(source, /def download\(self[^\n]*\) -> RawResponse\[bytes \| None\]:/); assert.doesNotMatch(source, /TypeAdapter\(models.DownloadBody\)/); }); @@ -154,10 +156,10 @@ test("Python facade validates and returns every successful response model", () = source.match(/TypeAdapter\(models\.SharedResponse\)/g)?.length, 2, ); - assert.match( - source, - /models\.CompletedResponse \| models\.PendingResponse \| RawResponse\[models\.CompletedResponse \| models\.PendingResponse\]/, - ); + // The client returns the decoded result; photon.raw returns the whole response. + assert.match(source, /\) -> models\.CompletedResponse \| models\.PendingResponse:/); + assert.match(source, /\) -> RawResponse\[models\.CompletedResponse \| models\.PendingResponse\]:/); + assert.doesNotMatch(source, /models\.PendingResponse \| RawResponse/); }); test("TypeScript facade validates each success status against its own component", () => { @@ -248,9 +250,10 @@ test("Python facade escapes reserved identifiers while preserving wire names", ( source, /field_2fa_code: str \| MISSING = Field\(default=MISSING, alias="2fa-code"\)/, ); - assert.equal(source.match(/def async_\(self,/g)?.length, 2); - assert.match(source, /self\.from_ = SyncFromResource\(transport, raw\)/); - assert.match(source, /self\.from_ = AsyncFromResource\(transport, raw\)/); + assert.equal(source.match(/def async_\(self,/g)?.length, 4); + for (const prefix of ["Sync", "SyncRaw", "Async", "AsyncRaw"]) { + assert.match(source, new RegExp(`self\\.from_ = ${prefix}FromResource\\(transport\\)`)); + } }); @@ -416,8 +419,9 @@ test("Python resources are snake_case while TypeScript keeps camelCase", () => { nested.namespace = ["projects", "agentProfile"]; const fixture = { operations: [nested] }; const python = renderPython(fixture); - assert.match(python, /self\.agent_profile = SyncProjectsAgentProfileResource\(transport, raw\)/); - assert.match(python, /self\.agent_profile = AsyncProjectsAgentProfileResource\(transport, raw\)/); + for (const prefix of ["Sync", "SyncRaw", "Async", "AsyncRaw"]) { + assert.match(python, new RegExp(`self\\.agent_profile = ${prefix}ProjectsAgentProfileResource\\(transport\\)`)); + } assert.doesNotMatch(python, /self\.agentProfile/); assert.match(renderTypeScript(fixture), /agentProfile: \{/); }); diff --git a/tools/openapi/src/generate-facades.ts b/tools/openapi/src/generate-facades.ts index 72d72cb..dc1a5c6 100644 --- a/tools/openapi/src/generate-facades.ts +++ b/tools/openapi/src/generate-facades.ts @@ -553,6 +553,7 @@ function pythonMethod( operation: ManifestOperation, asynchronous: boolean, methodName: string, + raw: boolean, ): string { const typeName = pascalCase(operation.operationId); const required = @@ -561,19 +562,25 @@ function pythonMethod( const input = required ? `input: ${typeName}Input` : `input: ${typeName}Input | None = None`; - const parsedInput = required ? "input" : `(input or ${typeName}Input())`; const outputType = pythonSuccessType(operation); const awaitPrefix = asynchronous ? "await " : ""; const asyncPrefix = asynchronous ? "async " : ""; + if (!raw) { + // The client's method returns the decoded result; its raw counterpart + // (photon.raw) holds the request and returns the whole response. + return ` ${asyncPrefix}def ${methodName}(self, ${input}) -> ${outputType}:\n` + + pythonDocumentation(operation) + + ` return (${awaitPrefix}self._raw_resource.${methodName}(input)).data\n`; + } + const parsedInput = required ? "input" : `(input or ${typeName}Input())`; const rawRequest = operationMedia(operation).rawRequest; - return ` ${asyncPrefix}def ${methodName}(self, ${input}) -> ${outputType} | RawResponse[${outputType}]:\n` + + return ` ${asyncPrefix}def ${methodName}(self, ${input}) -> RawResponse[${outputType}]:\n` + pythonDocumentation(operation) + ` payload = ${parsedInput}.model_dump(mode="json", by_alias=True, exclude_unset=True${rawRequest ? ', exclude={"body"}' : ""})\n` + (rawRequest ? ` payload["body"] = ${parsedInput}.body.root if ${parsedInput}.body is not None else None\n` : "") + - ` response = ${awaitPrefix}self._transport.request(\n` + + ` return ${awaitPrefix}self._transport.request(\n` + ` _OP_${snakeCase(operation.operationId).toUpperCase()}, payload\n` + - ` )\n` + - ` return response if self._raw else response.data\n`; + ` )\n`; } function allNodes( @@ -590,27 +597,32 @@ function allNodes( return result; } +/** Python class name of a resource; the raw variant returns RawResponse[T]. */ +function pythonResourceName(path: string[], asynchronous: boolean, raw: boolean): string { + return `${asynchronous ? "Async" : "Sync"}${raw ? "Raw" : ""}${path.map(pascalCase).join("")}Resource`; +} + function pythonResourceClass( node: TreeNode, path: string[], asynchronous: boolean, + raw: boolean, ): string { - const prefix = asynchronous ? "Async" : "Sync"; - const className = `${prefix}${path.map(pascalCase).join("")}Resource`; const transportType = asynchronous ? "AsyncTransport" : "SyncTransport"; const lines = [ - `class ${className}:`, - ` def __init__(self, transport: ${transportType}, raw: bool = False) -> None:`, - " self._transport = transport", - " self._raw = raw", + `class ${pythonResourceName(path, asynchronous, raw)}:`, + ` def __init__(self, transport: ${transportType}) -> None:`, + raw + ? " self._transport = transport" + : ` self._raw_resource = ${pythonResourceName(path, asynchronous, true)}(transport)`, ]; - const memberNames = new Set(["__init__", "_raw", "_transport"]); + // Both variants reserve the same names, so members are named identically. + const memberNames = new Set(["__init__", "_raw_resource", "_transport"]); for (const [name] of [...node.children].sort(([a], [b]) => a.localeCompare(b), )) { - const childClass = `${prefix}${[...path, name].map(pascalCase).join("")}Resource`; const memberName = allocatePythonIdentifier(name, memberNames, "resource"); - lines.push(` self.${memberName} = ${childClass}(transport, raw)`); + lines.push(` self.${memberName} = ${pythonResourceName([...path, name], asynchronous, raw)}(transport)`); } for (const operation of node.operations.sort((a, b) => a.rpcMethod.localeCompare(b.rpcMethod), @@ -622,18 +634,18 @@ function pythonResourceClass( ); lines.push( "", - pythonMethod(operation, asynchronous, methodName).trimEnd(), + pythonMethod(operation, asynchronous, methodName, raw).trimEnd(), ); } return `${lines.join("\n")}\n`; } -function pythonRoot(root: TreeNode, asynchronous: boolean): string { +function pythonRoot(root: TreeNode, asynchronous: boolean, raw: boolean): string { const prefix = asynchronous ? "Async" : "Sync"; const transportType = asynchronous ? "AsyncTransport" : "SyncTransport"; const lines = [ - `class ${prefix}Root:`, - ` def __init__(self, transport: ${transportType}, raw: bool = False) -> None:`, + `class ${prefix}${raw ? "Raw" : ""}Root:`, + ` def __init__(self, transport: ${transportType}) -> None:`, ]; const memberNames = new Set(["__init__"]); for (const [name] of [...root.children].sort(([a], [b]) => @@ -641,7 +653,7 @@ function pythonRoot(root: TreeNode, asynchronous: boolean): string { )) { const memberName = allocatePythonIdentifier(name, memberNames, "resource"); lines.push( - ` self.${memberName} = ${prefix}${pascalCase(name)}Resource(transport, raw)`, + ` self.${memberName} = ${pythonResourceName([name], asynchronous, raw)}(transport)`, ); } return `${lines.join("\n")}\n`; @@ -666,12 +678,11 @@ from .transport import AsyncTransport, OperationSpec, RawResponse, SyncTransport `; const inputClasses = operations.map(pythonInputClasses).join("\n"); const constants = operations.map(pythonOperationConstant).join("\n\n"); - const syncClasses = nodes - .map(({ node, path }) => pythonResourceClass(node, path, false)) - .join("\n"); - const asyncClasses = nodes - .map(({ node, path }) => pythonResourceClass(node, path, true)) + const classes = (asynchronous: boolean) => [true, false] + .flatMap((raw) => nodes.map(({ node, path }) => pythonResourceClass(node, path, asynchronous, raw))) .join("\n"); + const syncClasses = classes(false); + const asyncClasses = classes(true); return ( PYTHON_GENERATED_HEADER + imports + @@ -684,9 +695,13 @@ from .transport import AsyncTransport, OperationSpec, RawResponse, SyncTransport "\n" + asyncClasses + "\n" + - pythonRoot(tree, false) + + pythonRoot(tree, false, true) + + "\n" + + pythonRoot(tree, false, false) + + "\n" + + pythonRoot(tree, true, true) + "\n" + - pythonRoot(tree, true) + pythonRoot(tree, true, false) ); } diff --git a/tools/python-codegen/requirements-dev.txt b/tools/python-codegen/requirements-dev.txt index 9c36e7b..936691f 100644 --- a/tools/python-codegen/requirements-dev.txt +++ b/tools/python-codegen/requirements-dev.txt @@ -13,3 +13,5 @@ referencing==0.37.0 # Gate B reference (tools/conformance/reference.py): ECMA-262 patterns, as # JSON Schema 2020-12 specifies, so generated values are contract-valid. regress==2026.9.1 +# Static type check of the Python client's public typing (packages/python/tests/typing_check.py). +pyright==1.1.414