From 1197a1218644530a19719ab9866385a3155581d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:42:54 +0000 Subject: [PATCH 01/34] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/prelude_python_sdk/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/prelude_python_sdk/_types.py b/src/prelude_python_sdk/_types.py index e7c46a0..4bfba7c 100644 --- a/src/prelude_python_sdk/_types.py +++ b/src/prelude_python_sdk/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From 6fc2141b36efba6d357336a36c1305864ffec8cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:30:06 +0000 Subject: [PATCH 02/34] chore: add missing docstrings --- .../types/lookup_lookup_response.py | 4 ++++ ...notify_get_subscription_config_response.py | 2 ++ ...tify_list_subscription_configs_response.py | 2 ++ .../types/notify_send_batch_response.py | 4 ++++ .../types/verification_check_params.py | 5 +++++ .../types/verification_check_response.py | 2 ++ .../types/verification_create_params.py | 22 +++++++++++++++++++ .../types/verification_create_response.py | 4 ++++ ...ion_management_list_sender_ids_response.py | 2 ++ .../types/watch_predict_params.py | 9 ++++++++ .../types/watch_send_events_params.py | 2 ++ .../types/watch_send_feedbacks_params.py | 9 ++++++++ 12 files changed, 67 insertions(+) diff --git a/src/prelude_python_sdk/types/lookup_lookup_response.py b/src/prelude_python_sdk/types/lookup_lookup_response.py index f585823..48eedb6 100644 --- a/src/prelude_python_sdk/types/lookup_lookup_response.py +++ b/src/prelude_python_sdk/types/lookup_lookup_response.py @@ -9,6 +9,8 @@ class NetworkInfo(BaseModel): + """The current carrier information.""" + carrier_name: Optional[str] = None """The name of the carrier.""" @@ -20,6 +22,8 @@ class NetworkInfo(BaseModel): class OriginalNetworkInfo(BaseModel): + """The original carrier information.""" + carrier_name: Optional[str] = None """The name of the original carrier.""" diff --git a/src/prelude_python_sdk/types/notify_get_subscription_config_response.py b/src/prelude_python_sdk/types/notify_get_subscription_config_response.py index c6ec2cd..90ada18 100644 --- a/src/prelude_python_sdk/types/notify_get_subscription_config_response.py +++ b/src/prelude_python_sdk/types/notify_get_subscription_config_response.py @@ -9,6 +9,8 @@ class Messages(BaseModel): + """The subscription messages configuration.""" + help_message: Optional[str] = None """Message sent when user requests help.""" diff --git a/src/prelude_python_sdk/types/notify_list_subscription_configs_response.py b/src/prelude_python_sdk/types/notify_list_subscription_configs_response.py index eaa7947..b126ef1 100644 --- a/src/prelude_python_sdk/types/notify_list_subscription_configs_response.py +++ b/src/prelude_python_sdk/types/notify_list_subscription_configs_response.py @@ -9,6 +9,8 @@ class ConfigMessages(BaseModel): + """The subscription messages configuration.""" + help_message: Optional[str] = None """Message sent when user requests help.""" diff --git a/src/prelude_python_sdk/types/notify_send_batch_response.py b/src/prelude_python_sdk/types/notify_send_batch_response.py index 0130b74..3b1d882 100644 --- a/src/prelude_python_sdk/types/notify_send_batch_response.py +++ b/src/prelude_python_sdk/types/notify_send_batch_response.py @@ -11,6 +11,8 @@ class ResultError(BaseModel): + """Present only if success is false.""" + code: Optional[str] = None """The error code.""" @@ -19,6 +21,8 @@ class ResultError(BaseModel): class ResultMessage(BaseModel): + """Present only if success is true.""" + id: Optional[str] = None """The message identifier.""" diff --git a/src/prelude_python_sdk/types/verification_check_params.py b/src/prelude_python_sdk/types/verification_check_params.py index dea9702..3da954d 100644 --- a/src/prelude_python_sdk/types/verification_check_params.py +++ b/src/prelude_python_sdk/types/verification_check_params.py @@ -20,6 +20,11 @@ class VerificationCheckParams(TypedDict, total=False): class Target(TypedDict, total=False): + """The verification target. + + Either a phone number or an email address. To use the email verification feature contact us to discuss your use case. + """ + type: Required[Literal["phone_number", "email_address"]] """The type of the target. Either "phone_number" or "email_address".""" diff --git a/src/prelude_python_sdk/types/verification_check_response.py b/src/prelude_python_sdk/types/verification_check_response.py index 28754e2..96ba4f9 100644 --- a/src/prelude_python_sdk/types/verification_check_response.py +++ b/src/prelude_python_sdk/types/verification_check_response.py @@ -9,6 +9,8 @@ class Metadata(BaseModel): + """The metadata for this verification.""" + correlation_id: Optional[str] = None """A user-defined identifier to correlate this verification with. diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index eba2a33..9422039 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -38,6 +38,11 @@ class VerificationCreateParams(TypedDict, total=False): class Target(TypedDict, total=False): + """The verification target. + + Either a phone number or an email address. To use the email verification feature contact us to discuss your use case. + """ + type: Required[Literal["phone_number", "email_address"]] """The type of the target. Either "phone_number" or "email_address".""" @@ -46,6 +51,11 @@ class Target(TypedDict, total=False): class Metadata(TypedDict, total=False): + """The metadata for this verification. + + This object will be returned with every response or webhook sent that refers to this verification. + """ + correlation_id: str """A user-defined identifier to correlate this verification with. @@ -55,6 +65,11 @@ class Metadata(TypedDict, total=False): class OptionsAppRealm(TypedDict, total=False): + """This allows you to automatically retrieve and fill the OTP code on mobile apps. + + Currently only Android devices are supported. + """ + platform: Required[Literal["android"]] """The platform the SMS will be sent to. @@ -70,6 +85,8 @@ class OptionsAppRealm(TypedDict, total=False): class Options(TypedDict, total=False): + """Verification options""" + app_realm: OptionsAppRealm """This allows you to automatically retrieve and fill the OTP code on mobile apps. @@ -140,6 +157,11 @@ class Options(TypedDict, total=False): class Signals(TypedDict, total=False): + """The signals used for anti-fraud. + + For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). + """ + app_version: str """The version of your application.""" diff --git a/src/prelude_python_sdk/types/verification_create_response.py b/src/prelude_python_sdk/types/verification_create_response.py index 91e02bc..08cfbe7 100644 --- a/src/prelude_python_sdk/types/verification_create_response.py +++ b/src/prelude_python_sdk/types/verification_create_response.py @@ -9,6 +9,8 @@ class Metadata(BaseModel): + """The metadata for this verification.""" + correlation_id: Optional[str] = None """A user-defined identifier to correlate this verification with. @@ -18,6 +20,8 @@ class Metadata(BaseModel): class Silent(BaseModel): + """The silent verification specific properties.""" + request_url: str """The URL to start the silent verification towards.""" diff --git a/src/prelude_python_sdk/types/verification_management_list_sender_ids_response.py b/src/prelude_python_sdk/types/verification_management_list_sender_ids_response.py index b918111..8ab4ef8 100644 --- a/src/prelude_python_sdk/types/verification_management_list_sender_ids_response.py +++ b/src/prelude_python_sdk/types/verification_management_list_sender_ids_response.py @@ -22,4 +22,6 @@ class SenderID(BaseModel): class VerificationManagementListSenderIDsResponse(BaseModel): + """A list of Sender ID.""" + sender_ids: Optional[List[SenderID]] = None diff --git a/src/prelude_python_sdk/types/watch_predict_params.py b/src/prelude_python_sdk/types/watch_predict_params.py index b341838..1d6de12 100644 --- a/src/prelude_python_sdk/types/watch_predict_params.py +++ b/src/prelude_python_sdk/types/watch_predict_params.py @@ -26,6 +26,8 @@ class WatchPredictParams(TypedDict, total=False): class Target(TypedDict, total=False): + """The prediction target. Only supports phone numbers for now.""" + type: Required[Literal["phone_number", "email_address"]] """The type of the target. Either "phone_number" or "email_address".""" @@ -34,6 +36,8 @@ class Target(TypedDict, total=False): class Metadata(TypedDict, total=False): + """The metadata for this prediction.""" + correlation_id: str """A user-defined identifier to correlate this prediction with. @@ -43,6 +47,11 @@ class Metadata(TypedDict, total=False): class Signals(TypedDict, total=False): + """The signals used for anti-fraud. + + For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). + """ + app_version: str """The version of your application.""" diff --git a/src/prelude_python_sdk/types/watch_send_events_params.py b/src/prelude_python_sdk/types/watch_send_events_params.py index ab5b492..949b93d 100644 --- a/src/prelude_python_sdk/types/watch_send_events_params.py +++ b/src/prelude_python_sdk/types/watch_send_events_params.py @@ -14,6 +14,8 @@ class WatchSendEventsParams(TypedDict, total=False): class EventTarget(TypedDict, total=False): + """The event target. Only supports phone numbers for now.""" + type: Required[Literal["phone_number", "email_address"]] """The type of the target. Either "phone_number" or "email_address".""" diff --git a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py index 1367260..d728e72 100644 --- a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py +++ b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py @@ -14,6 +14,8 @@ class WatchSendFeedbacksParams(TypedDict, total=False): class FeedbackTarget(TypedDict, total=False): + """The feedback target. Only supports phone numbers for now.""" + type: Required[Literal["phone_number", "email_address"]] """The type of the target. Either "phone_number" or "email_address".""" @@ -22,6 +24,8 @@ class FeedbackTarget(TypedDict, total=False): class FeedbackMetadata(TypedDict, total=False): + """The metadata for this feedback.""" + correlation_id: str """A user-defined identifier to correlate this feedback with. @@ -31,6 +35,11 @@ class FeedbackMetadata(TypedDict, total=False): class FeedbackSignals(TypedDict, total=False): + """The signals used for anti-fraud. + + For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). + """ + app_version: str """The version of your application.""" From 84ade2c96740cc217646929248a0b4e60c2434f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:20:57 +0000 Subject: [PATCH 03/34] chore(internal): add missing files argument to base client --- src/prelude_python_sdk/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/prelude_python_sdk/_base_client.py b/src/prelude_python_sdk/_base_client.py index f3d952b..869e3b0 100644 --- a/src/prelude_python_sdk/_base_client.py +++ b/src/prelude_python_sdk/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From 5300f18a10c0db7b222c1bb3e487402aab9d318e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:23:54 +0000 Subject: [PATCH 04/34] chore: speedup initial import --- src/prelude_python_sdk/_client.py | 323 +++++++++++++++++++++++------- 1 file changed, 255 insertions(+), 68 deletions(-) diff --git a/src/prelude_python_sdk/_client.py b/src/prelude_python_sdk/_client.py index 95c5c63..a212479 100644 --- a/src/prelude_python_sdk/_client.py +++ b/src/prelude_python_sdk/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import watch, lookup, notify, verification, transactional, verification_management from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import PreludeError, APIStatusError from ._base_client import ( @@ -30,19 +30,19 @@ AsyncAPIClient, ) +if TYPE_CHECKING: + from .resources import watch, lookup, notify, verification, transactional, verification_management + from .resources.watch import WatchResource, AsyncWatchResource + from .resources.lookup import LookupResource, AsyncLookupResource + from .resources.notify import NotifyResource, AsyncNotifyResource + from .resources.verification import VerificationResource, AsyncVerificationResource + from .resources.transactional import TransactionalResource, AsyncTransactionalResource + from .resources.verification_management import VerificationManagementResource, AsyncVerificationManagementResource + __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Prelude", "AsyncPrelude", "Client", "AsyncClient"] class Prelude(SyncAPIClient): - lookup: lookup.LookupResource - notify: notify.NotifyResource - transactional: transactional.TransactionalResource - verification: verification.VerificationResource - verification_management: verification_management.VerificationManagementResource - watch: watch.WatchResource - with_raw_response: PreludeWithRawResponse - with_streaming_response: PreludeWithStreamedResponse - # client options api_token: str @@ -97,14 +97,49 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.lookup = lookup.LookupResource(self) - self.notify = notify.NotifyResource(self) - self.transactional = transactional.TransactionalResource(self) - self.verification = verification.VerificationResource(self) - self.verification_management = verification_management.VerificationManagementResource(self) - self.watch = watch.WatchResource(self) - self.with_raw_response = PreludeWithRawResponse(self) - self.with_streaming_response = PreludeWithStreamedResponse(self) + @cached_property + def lookup(self) -> LookupResource: + from .resources.lookup import LookupResource + + return LookupResource(self) + + @cached_property + def notify(self) -> NotifyResource: + from .resources.notify import NotifyResource + + return NotifyResource(self) + + @cached_property + def transactional(self) -> TransactionalResource: + from .resources.transactional import TransactionalResource + + return TransactionalResource(self) + + @cached_property + def verification(self) -> VerificationResource: + from .resources.verification import VerificationResource + + return VerificationResource(self) + + @cached_property + def verification_management(self) -> VerificationManagementResource: + from .resources.verification_management import VerificationManagementResource + + return VerificationManagementResource(self) + + @cached_property + def watch(self) -> WatchResource: + from .resources.watch import WatchResource + + return WatchResource(self) + + @cached_property + def with_raw_response(self) -> PreludeWithRawResponse: + return PreludeWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PreludeWithStreamedResponse: + return PreludeWithStreamedResponse(self) @property @override @@ -212,15 +247,6 @@ def _make_status_error( class AsyncPrelude(AsyncAPIClient): - lookup: lookup.AsyncLookupResource - notify: notify.AsyncNotifyResource - transactional: transactional.AsyncTransactionalResource - verification: verification.AsyncVerificationResource - verification_management: verification_management.AsyncVerificationManagementResource - watch: watch.AsyncWatchResource - with_raw_response: AsyncPreludeWithRawResponse - with_streaming_response: AsyncPreludeWithStreamedResponse - # client options api_token: str @@ -275,14 +301,49 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.lookup = lookup.AsyncLookupResource(self) - self.notify = notify.AsyncNotifyResource(self) - self.transactional = transactional.AsyncTransactionalResource(self) - self.verification = verification.AsyncVerificationResource(self) - self.verification_management = verification_management.AsyncVerificationManagementResource(self) - self.watch = watch.AsyncWatchResource(self) - self.with_raw_response = AsyncPreludeWithRawResponse(self) - self.with_streaming_response = AsyncPreludeWithStreamedResponse(self) + @cached_property + def lookup(self) -> AsyncLookupResource: + from .resources.lookup import AsyncLookupResource + + return AsyncLookupResource(self) + + @cached_property + def notify(self) -> AsyncNotifyResource: + from .resources.notify import AsyncNotifyResource + + return AsyncNotifyResource(self) + + @cached_property + def transactional(self) -> AsyncTransactionalResource: + from .resources.transactional import AsyncTransactionalResource + + return AsyncTransactionalResource(self) + + @cached_property + def verification(self) -> AsyncVerificationResource: + from .resources.verification import AsyncVerificationResource + + return AsyncVerificationResource(self) + + @cached_property + def verification_management(self) -> AsyncVerificationManagementResource: + from .resources.verification_management import AsyncVerificationManagementResource + + return AsyncVerificationManagementResource(self) + + @cached_property + def watch(self) -> AsyncWatchResource: + from .resources.watch import AsyncWatchResource + + return AsyncWatchResource(self) + + @cached_property + def with_raw_response(self) -> AsyncPreludeWithRawResponse: + return AsyncPreludeWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPreludeWithStreamedResponse: + return AsyncPreludeWithStreamedResponse(self) @property @override @@ -390,51 +451,177 @@ def _make_status_error( class PreludeWithRawResponse: + _client: Prelude + def __init__(self, client: Prelude) -> None: - self.lookup = lookup.LookupResourceWithRawResponse(client.lookup) - self.notify = notify.NotifyResourceWithRawResponse(client.notify) - self.transactional = transactional.TransactionalResourceWithRawResponse(client.transactional) - self.verification = verification.VerificationResourceWithRawResponse(client.verification) - self.verification_management = verification_management.VerificationManagementResourceWithRawResponse( - client.verification_management - ) - self.watch = watch.WatchResourceWithRawResponse(client.watch) + self._client = client + + @cached_property + def lookup(self) -> lookup.LookupResourceWithRawResponse: + from .resources.lookup import LookupResourceWithRawResponse + + return LookupResourceWithRawResponse(self._client.lookup) + + @cached_property + def notify(self) -> notify.NotifyResourceWithRawResponse: + from .resources.notify import NotifyResourceWithRawResponse + + return NotifyResourceWithRawResponse(self._client.notify) + + @cached_property + def transactional(self) -> transactional.TransactionalResourceWithRawResponse: + from .resources.transactional import TransactionalResourceWithRawResponse + + return TransactionalResourceWithRawResponse(self._client.transactional) + + @cached_property + def verification(self) -> verification.VerificationResourceWithRawResponse: + from .resources.verification import VerificationResourceWithRawResponse + + return VerificationResourceWithRawResponse(self._client.verification) + + @cached_property + def verification_management(self) -> verification_management.VerificationManagementResourceWithRawResponse: + from .resources.verification_management import VerificationManagementResourceWithRawResponse + + return VerificationManagementResourceWithRawResponse(self._client.verification_management) + + @cached_property + def watch(self) -> watch.WatchResourceWithRawResponse: + from .resources.watch import WatchResourceWithRawResponse + + return WatchResourceWithRawResponse(self._client.watch) class AsyncPreludeWithRawResponse: + _client: AsyncPrelude + def __init__(self, client: AsyncPrelude) -> None: - self.lookup = lookup.AsyncLookupResourceWithRawResponse(client.lookup) - self.notify = notify.AsyncNotifyResourceWithRawResponse(client.notify) - self.transactional = transactional.AsyncTransactionalResourceWithRawResponse(client.transactional) - self.verification = verification.AsyncVerificationResourceWithRawResponse(client.verification) - self.verification_management = verification_management.AsyncVerificationManagementResourceWithRawResponse( - client.verification_management - ) - self.watch = watch.AsyncWatchResourceWithRawResponse(client.watch) + self._client = client + + @cached_property + def lookup(self) -> lookup.AsyncLookupResourceWithRawResponse: + from .resources.lookup import AsyncLookupResourceWithRawResponse + + return AsyncLookupResourceWithRawResponse(self._client.lookup) + + @cached_property + def notify(self) -> notify.AsyncNotifyResourceWithRawResponse: + from .resources.notify import AsyncNotifyResourceWithRawResponse + + return AsyncNotifyResourceWithRawResponse(self._client.notify) + + @cached_property + def transactional(self) -> transactional.AsyncTransactionalResourceWithRawResponse: + from .resources.transactional import AsyncTransactionalResourceWithRawResponse + + return AsyncTransactionalResourceWithRawResponse(self._client.transactional) + + @cached_property + def verification(self) -> verification.AsyncVerificationResourceWithRawResponse: + from .resources.verification import AsyncVerificationResourceWithRawResponse + + return AsyncVerificationResourceWithRawResponse(self._client.verification) + + @cached_property + def verification_management(self) -> verification_management.AsyncVerificationManagementResourceWithRawResponse: + from .resources.verification_management import AsyncVerificationManagementResourceWithRawResponse + + return AsyncVerificationManagementResourceWithRawResponse(self._client.verification_management) + + @cached_property + def watch(self) -> watch.AsyncWatchResourceWithRawResponse: + from .resources.watch import AsyncWatchResourceWithRawResponse + + return AsyncWatchResourceWithRawResponse(self._client.watch) class PreludeWithStreamedResponse: + _client: Prelude + def __init__(self, client: Prelude) -> None: - self.lookup = lookup.LookupResourceWithStreamingResponse(client.lookup) - self.notify = notify.NotifyResourceWithStreamingResponse(client.notify) - self.transactional = transactional.TransactionalResourceWithStreamingResponse(client.transactional) - self.verification = verification.VerificationResourceWithStreamingResponse(client.verification) - self.verification_management = verification_management.VerificationManagementResourceWithStreamingResponse( - client.verification_management - ) - self.watch = watch.WatchResourceWithStreamingResponse(client.watch) + self._client = client + + @cached_property + def lookup(self) -> lookup.LookupResourceWithStreamingResponse: + from .resources.lookup import LookupResourceWithStreamingResponse + + return LookupResourceWithStreamingResponse(self._client.lookup) + + @cached_property + def notify(self) -> notify.NotifyResourceWithStreamingResponse: + from .resources.notify import NotifyResourceWithStreamingResponse + + return NotifyResourceWithStreamingResponse(self._client.notify) + + @cached_property + def transactional(self) -> transactional.TransactionalResourceWithStreamingResponse: + from .resources.transactional import TransactionalResourceWithStreamingResponse + + return TransactionalResourceWithStreamingResponse(self._client.transactional) + + @cached_property + def verification(self) -> verification.VerificationResourceWithStreamingResponse: + from .resources.verification import VerificationResourceWithStreamingResponse + + return VerificationResourceWithStreamingResponse(self._client.verification) + + @cached_property + def verification_management(self) -> verification_management.VerificationManagementResourceWithStreamingResponse: + from .resources.verification_management import VerificationManagementResourceWithStreamingResponse + + return VerificationManagementResourceWithStreamingResponse(self._client.verification_management) + + @cached_property + def watch(self) -> watch.WatchResourceWithStreamingResponse: + from .resources.watch import WatchResourceWithStreamingResponse + + return WatchResourceWithStreamingResponse(self._client.watch) class AsyncPreludeWithStreamedResponse: + _client: AsyncPrelude + def __init__(self, client: AsyncPrelude) -> None: - self.lookup = lookup.AsyncLookupResourceWithStreamingResponse(client.lookup) - self.notify = notify.AsyncNotifyResourceWithStreamingResponse(client.notify) - self.transactional = transactional.AsyncTransactionalResourceWithStreamingResponse(client.transactional) - self.verification = verification.AsyncVerificationResourceWithStreamingResponse(client.verification) - self.verification_management = verification_management.AsyncVerificationManagementResourceWithStreamingResponse( - client.verification_management - ) - self.watch = watch.AsyncWatchResourceWithStreamingResponse(client.watch) + self._client = client + + @cached_property + def lookup(self) -> lookup.AsyncLookupResourceWithStreamingResponse: + from .resources.lookup import AsyncLookupResourceWithStreamingResponse + + return AsyncLookupResourceWithStreamingResponse(self._client.lookup) + + @cached_property + def notify(self) -> notify.AsyncNotifyResourceWithStreamingResponse: + from .resources.notify import AsyncNotifyResourceWithStreamingResponse + + return AsyncNotifyResourceWithStreamingResponse(self._client.notify) + + @cached_property + def transactional(self) -> transactional.AsyncTransactionalResourceWithStreamingResponse: + from .resources.transactional import AsyncTransactionalResourceWithStreamingResponse + + return AsyncTransactionalResourceWithStreamingResponse(self._client.transactional) + + @cached_property + def verification(self) -> verification.AsyncVerificationResourceWithStreamingResponse: + from .resources.verification import AsyncVerificationResourceWithStreamingResponse + + return AsyncVerificationResourceWithStreamingResponse(self._client.verification) + + @cached_property + def verification_management( + self, + ) -> verification_management.AsyncVerificationManagementResourceWithStreamingResponse: + from .resources.verification_management import AsyncVerificationManagementResourceWithStreamingResponse + + return AsyncVerificationManagementResourceWithStreamingResponse(self._client.verification_management) + + @cached_property + def watch(self) -> watch.AsyncWatchResourceWithStreamingResponse: + from .resources.watch import AsyncWatchResourceWithStreamingResponse + + return AsyncWatchResourceWithStreamingResponse(self._client.watch) Client = Prelude From 2a4c51aa40b31c9709fd482771a5eb1defd60786 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:49:25 +0000 Subject: [PATCH 05/34] fix: use async_to_httpx_files in patch method --- src/prelude_python_sdk/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prelude_python_sdk/_base_client.py b/src/prelude_python_sdk/_base_client.py index 869e3b0..37011cf 100644 --- a/src/prelude_python_sdk/_base_client.py +++ b/src/prelude_python_sdk/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From aaa77d73edce4cc03a4cd203b90a7c6f9e145bbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:55:56 +0000 Subject: [PATCH 06/34] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index ffa4a0f..ab49711 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import prelude_python_sdk' From 72f7709ffeae6b6b4ad835f0861136d713aa3ba5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:07:33 +0000 Subject: [PATCH 07/34] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 69f0a67..ed78459 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Prelude + Copyright 2026 Prelude Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 6ab992cc4da735868c0aed05cd737ed33f2c8d68 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:35:39 +0000 Subject: [PATCH 08/34] feat(api): api update --- .stats.yml | 4 +- .../types/watch_predict_response.py | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3559e65..4eb0dc3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-fe14bed3c1b6245444e1a53e7b0b172301c207ef9500dc68f4d8e58a7bfec436.yml -openapi_spec_hash: 4acdc9dd487011b2391f6fe02812a6bd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-b78e38d4b818da748071fa575b363c28b4b08fedfde27dc5e52501946096aed2.yml +openapi_spec_hash: dac70ea7cba2eb2bcc5c73c0a825b956 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/watch_predict_response.py b/src/prelude_python_sdk/types/watch_predict_response.py index 0bd330c..906108d 100644 --- a/src/prelude_python_sdk/types/watch_predict_response.py +++ b/src/prelude_python_sdk/types/watch_predict_response.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import List, Optional from typing_extensions import Literal from .._models import BaseModel @@ -19,3 +20,46 @@ class WatchPredictResponse(BaseModel): Report it back to us to help us diagnose your issues. """ + + risk_factors: Optional[ + List[ + Literal[ + "behavioral_pattern", + "device_attribute", + "fraud_database", + "location_discrepancy", + "network_fingerprint", + "poor_conversion_history", + "prefix_concentration", + "suspected_request_tampering", + "suspicious_ip_address", + "temporary_phone_number", + ] + ] + ] = None + """The risk factors that contributed to the suspicious prediction. + + Only present when prediction is "suspicious" and the anti-fraud system detected + specific risk signals. + + - `behavioral_pattern` - The phone number past behavior during verification + flows exhibits suspicious patterns. + - `device_attribute` - The device exhibits characteristics associated with + suspicious activity patterns. + - `fraud_database` - The phone number has been flagged as suspicious in one or + more of our fraud databases. + - `location_discrepancy` - The phone number prefix and IP address discrepancy + indicates potential fraud. + - `network_fingerprint` - The network connection exhibits characteristics + associated with suspicious activity patterns. + - `poor_conversion_history` - The phone number has a history of poorly + converting to a verified phone number. + - `prefix_concentration` - The phone number is part of a range known to be + associated with suspicious activity patterns. + - `suspected_request_tampering` - The SDK signature is invalid and the request + is considered to be tampered with. + - `suspicious_ip_address` - The IP address is deemed to be associated with + suspicious activity patterns. + - `temporary_phone_number` - The phone number is known to be a temporary or + disposable number. + """ From 94f4fa9904573e950ae6d01d289cce34e9e70370 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:55:37 +0000 Subject: [PATCH 09/34] feat(api): api update --- .stats.yml | 4 ++-- .../types/verification_create_params.py | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4eb0dc3..81b0ffb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-b78e38d4b818da748071fa575b363c28b4b08fedfde27dc5e52501946096aed2.yml -openapi_spec_hash: dac70ea7cba2eb2bcc5c73c0a825b956 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-6dd3681d80a10e78909c7761f420809f96bdb50410f0ef42eb83b79bab677ceb.yml +openapi_spec_hash: 7ec2075e9fb3e13181f997bd2366db34 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index 9422039..4d989be 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -65,22 +65,24 @@ class Metadata(TypedDict, total=False): class OptionsAppRealm(TypedDict, total=False): - """This allows you to automatically retrieve and fill the OTP code on mobile apps. + """This allows automatic OTP retrieval on mobile apps and web browsers. - Currently only Android devices are supported. + Supported platforms are Android (SMS Retriever API) and Web (WebOTP API). """ - platform: Required[Literal["android"]] - """The platform the SMS will be sent to. + platform: Required[Literal["android", "web"]] + """The platform for automatic OTP retrieval. - We are currently only supporting "android". + Use "android" for the SMS Retriever API or "web" for the WebOTP API. """ value: Required[str] - """ - The Android SMS Retriever API hash code that identifies your app. For more - information, see - [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + """The value depends on the platform: + + - For Android: The SMS Retriever API hash code (11 characters). See + [Google documentation](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string). + - For Web: The origin domain (e.g., "example.com" or "www.example.com"). See + [WebOTP API documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API). """ @@ -88,9 +90,9 @@ class Options(TypedDict, total=False): """Verification options""" app_realm: OptionsAppRealm - """This allows you to automatically retrieve and fill the OTP code on mobile apps. + """This allows automatic OTP retrieval on mobile apps and web browsers. - Currently only Android devices are supported. + Supported platforms are Android (SMS Retriever API) and Web (WebOTP API). """ callback_url: str From 5cc25b3416e9fe458d27fb26a0f8ebca1c36312a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:31:00 +0000 Subject: [PATCH 10/34] feat(client): add support for binary request streaming --- src/prelude_python_sdk/_base_client.py | 145 +++++++++++++++++-- src/prelude_python_sdk/_models.py | 17 ++- src/prelude_python_sdk/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/prelude_python_sdk/_base_client.py b/src/prelude_python_sdk/_base_client.py index 37011cf..83c50bb 100644 --- a/src/prelude_python_sdk/_base_client.py +++ b/src/prelude_python_sdk/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/prelude_python_sdk/_models.py b/src/prelude_python_sdk/_models.py index ca9500b..29070e0 100644 --- a/src/prelude_python_sdk/_models.py +++ b/src/prelude_python_sdk/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/prelude_python_sdk/_types.py b/src/prelude_python_sdk/_types.py index 4bfba7c..b58f49d 100644 --- a/src/prelude_python_sdk/_types.py +++ b/src/prelude_python_sdk/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index c7e59df..ed835f2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_token = "My API Token" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Prelude | AsyncPrelude) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -505,6 +558,70 @@ def test_multipart_repeating_array(self, client: Prelude) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Prelude) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Prelude( + base_url=base_url, + api_token=api_token, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Prelude) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Prelude) -> None: class Model1(BaseModel): @@ -1364,6 +1481,72 @@ def test_multipart_repeating_array(self, async_client: AsyncPrelude) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncPrelude) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncPrelude( + base_url=base_url, + api_token=api_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncPrelude + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncPrelude) -> None: class Model1(BaseModel): From f7afece5c9ea5bb3f3420a8a1f543da1e136bcff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:12:38 +0000 Subject: [PATCH 11/34] feat(api): api update --- .stats.yml | 4 ++-- .../types/verification_create_params.py | 24 ++++++++++++------- .../types/watch_predict_params.py | 24 ++++++++++++------- .../types/watch_send_feedbacks_params.py | 24 ++++++++++++------- tests/api_resources/test_verification.py | 4 ++-- tests/api_resources/test_watch.py | 4 ++-- 6 files changed, 51 insertions(+), 33 deletions(-) diff --git a/.stats.yml b/.stats.yml index 81b0ffb..2398759 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-6dd3681d80a10e78909c7761f420809f96bdb50410f0ef42eb83b79bab677ceb.yml -openapi_spec_hash: 7ec2075e9fb3e13181f997bd2366db34 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-96235ac3b2a35068e6949a7ec4ae261fc69faa6ef88445b0e1bf1c0f95ae0ba0.yml +openapi_spec_hash: c97ca3aee349908f24ca982beaa90fc9 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index 4d989be..bbd051c 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -168,9 +168,10 @@ class Signals(TypedDict, total=False): """The version of your application.""" device_id: str - """The unique identifier for the user's device. + """A unique ID for the user's device. - For Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds + You should ensure that each user device has a unique `device_id` value. Ideally, + for Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. """ @@ -181,21 +182,26 @@ class Signals(TypedDict, total=False): """The type of the user's device.""" ip: str - """The IP address of the user's device.""" + """The public IP v4 or v6 address of the end-user's device. + + You should collect this from your backend. If your backend is behind a proxy, + use the `X-Forwarded-For`, `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or + an equivalent header to get the actual public IP of the end-user's device. + """ is_trusted_user: bool """ - This signal should provide a higher level of trust, indicating that the user is - genuine. Contact us to discuss your use case. For more details, refer to + This signal should indicate a higher level of trust, explicitly stating that the + user is genuine. Contact us to discuss your use case. For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). """ ja4_fingerprint: str - """The JA4 fingerprint observed for the connection. + """The JA4 fingerprint observed for the end-user's connection. - Prelude will infer it automatically when requests go through our client SDK - (which uses Prelude's edge), but you can also provide it explicitly if you - terminate TLS yourself. + Prelude will infer it automatically when you use our Frontend SDKs (which use + Prelude's edge network), but you can also forward the value if you terminate TLS + yourself. """ os_version: str diff --git a/src/prelude_python_sdk/types/watch_predict_params.py b/src/prelude_python_sdk/types/watch_predict_params.py index 1d6de12..18bfb76 100644 --- a/src/prelude_python_sdk/types/watch_predict_params.py +++ b/src/prelude_python_sdk/types/watch_predict_params.py @@ -56,9 +56,10 @@ class Signals(TypedDict, total=False): """The version of your application.""" device_id: str - """The unique identifier for the user's device. + """A unique ID for the user's device. - For Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds + You should ensure that each user device has a unique `device_id` value. Ideally, + for Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. """ @@ -69,21 +70,26 @@ class Signals(TypedDict, total=False): """The type of the user's device.""" ip: str - """The IP address of the user's device.""" + """The public IP v4 or v6 address of the end-user's device. + + You should collect this from your backend. If your backend is behind a proxy, + use the `X-Forwarded-For`, `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or + an equivalent header to get the actual public IP of the end-user's device. + """ is_trusted_user: bool """ - This signal should provide a higher level of trust, indicating that the user is - genuine. Contact us to discuss your use case. For more details, refer to + This signal should indicate a higher level of trust, explicitly stating that the + user is genuine. Contact us to discuss your use case. For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). """ ja4_fingerprint: str - """The JA4 fingerprint observed for the connection. + """The JA4 fingerprint observed for the end-user's connection. - Prelude will infer it automatically when requests go through our client SDK - (which uses Prelude's edge), but you can also provide it explicitly if you - terminate TLS yourself. + Prelude will infer it automatically when you use our Frontend SDKs (which use + Prelude's edge network), but you can also forward the value if you terminate TLS + yourself. """ os_version: str diff --git a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py index d728e72..c15e57d 100644 --- a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py +++ b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py @@ -44,9 +44,10 @@ class FeedbackSignals(TypedDict, total=False): """The version of your application.""" device_id: str - """The unique identifier for the user's device. + """A unique ID for the user's device. - For Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds + You should ensure that each user device has a unique `device_id` value. Ideally, + for Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds to the `identifierForVendor`. """ @@ -57,21 +58,26 @@ class FeedbackSignals(TypedDict, total=False): """The type of the user's device.""" ip: str - """The IP address of the user's device.""" + """The public IP v4 or v6 address of the end-user's device. + + You should collect this from your backend. If your backend is behind a proxy, + use the `X-Forwarded-For`, `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or + an equivalent header to get the actual public IP of the end-user's device. + """ is_trusted_user: bool """ - This signal should provide a higher level of trust, indicating that the user is - genuine. Contact us to discuss your use case. For more details, refer to + This signal should indicate a higher level of trust, explicitly stating that the + user is genuine. Contact us to discuss your use case. For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). """ ja4_fingerprint: str - """The JA4 fingerprint observed for the connection. + """The JA4 fingerprint observed for the end-user's connection. - Prelude will infer it automatically when requests go through our client SDK - (which uses Prelude's edge), but you can also provide it explicitly if you - terminate TLS yourself. + Prelude will infer it automatically when you use our Frontend SDKs (which use + Prelude's edge network), but you can also forward the value if you terminate TLS + yourself. """ os_version: str diff --git a/tests/api_resources/test_verification.py b/tests/api_resources/test_verification.py index 8e7c7ec..c08cf6e 100644 --- a/tests/api_resources/test_verification.py +++ b/tests/api_resources/test_verification.py @@ -62,7 +62,7 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", "device_platform": "ios", - "ip": "192.0.2.1", + "ip": "203.0.113.123", "is_trusted_user": False, "ja4_fingerprint": "t13d1516h2_8daaf6152771_e5627efa2ab1", "os_version": "18.0.1", @@ -194,7 +194,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", "device_platform": "ios", - "ip": "192.0.2.1", + "ip": "203.0.113.123", "is_trusted_user": False, "ja4_fingerprint": "t13d1516h2_8daaf6152771_e5627efa2ab1", "os_version": "18.0.1", diff --git a/tests/api_resources/test_watch.py b/tests/api_resources/test_watch.py index 51d933c..5d074d8 100644 --- a/tests/api_resources/test_watch.py +++ b/tests/api_resources/test_watch.py @@ -45,7 +45,7 @@ def test_method_predict_with_all_params(self, client: Prelude) -> None: "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", "device_platform": "ios", - "ip": "192.0.2.1", + "ip": "203.0.113.123", "is_trusted_user": False, "ja4_fingerprint": "t13d1516h2_8daaf6152771_e5627efa2ab1", "os_version": "18.0.1", @@ -227,7 +227,7 @@ async def test_method_predict_with_all_params(self, async_client: AsyncPrelude) "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", "device_platform": "ios", - "ip": "192.0.2.1", + "ip": "203.0.113.123", "is_trusted_user": False, "ja4_fingerprint": "t13d1516h2_8daaf6152771_e5627efa2ab1", "os_version": "18.0.1", From 34bf2ba6a9e133d5287719459da01be9289b115d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:31:04 +0000 Subject: [PATCH 12/34] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36a46e6..73046fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/prelude-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/prelude-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/prelude-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 1ccb8d9..44394c8 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 82d8ed1..e23c857 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'prelude-so/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From 37e674a17a76e2a0bb3a2f9b29439ea233830109 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:10:23 +0000 Subject: [PATCH 13/34] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73046fb..a6a4815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/prelude-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From c00e5f5f604a94604140731637d7fb975376188e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:26:30 +0000 Subject: [PATCH 14/34] feat(api): api update --- .stats.yml | 4 ++-- .../types/notify_send_batch_response.py | 16 ++++++++++++++++ .../types/notify_send_response.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2398759..d725f1b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-96235ac3b2a35068e6949a7ec4ae261fc69faa6ef88445b0e1bf1c0f95ae0ba0.yml -openapi_spec_hash: c97ca3aee349908f24ca982beaa90fc9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ae06b3bf0c78b49f67ba55dd910b013e4cd9b8c8b97896734354d8f233a4f5bb.yml +openapi_spec_hash: 129ee2e95ca86f1fbef1f61ca4599c66 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/notify_send_batch_response.py b/src/prelude_python_sdk/types/notify_send_batch_response.py index 3b1d882..34af373 100644 --- a/src/prelude_python_sdk/types/notify_send_batch_response.py +++ b/src/prelude_python_sdk/types/notify_send_batch_response.py @@ -2,6 +2,7 @@ from typing import Dict, List, Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -32,6 +33,21 @@ class ResultMessage(BaseModel): created_at: Optional[datetime] = None """The message creation date in RFC3339 format.""" + encoding: Optional[Literal["GSM-7", "UCS-2"]] = None + """The SMS encoding type based on message content. + + GSM-7 supports standard characters (up to 160 chars per segment), while UCS-2 + supports Unicode including emoji (up to 70 chars per segment). Only present for + SMS messages. + """ + + estimated_segment_count: Optional[int] = None + """The estimated number of SMS segments for this message. + + This value is not contractual; the actual segment count will be determined after + the SMS is sent by the provider. Only present for SMS messages. + """ + expires_at: Optional[datetime] = None """The message expiration date in RFC3339 format.""" diff --git a/src/prelude_python_sdk/types/notify_send_response.py b/src/prelude_python_sdk/types/notify_send_response.py index c554c0e..f19a0f2 100644 --- a/src/prelude_python_sdk/types/notify_send_response.py +++ b/src/prelude_python_sdk/types/notify_send_response.py @@ -2,6 +2,7 @@ from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -35,6 +36,21 @@ class NotifySendResponse(BaseModel): correlation_id: Optional[str] = None """A user-defined identifier to correlate this message with your internal systems.""" + encoding: Optional[Literal["GSM-7", "UCS-2"]] = None + """The SMS encoding type based on message content. + + GSM-7 supports standard characters (up to 160 chars per segment), while UCS-2 + supports Unicode including emoji (up to 70 chars per segment). Only present for + SMS messages. + """ + + estimated_segment_count: Optional[int] = None + """The estimated number of SMS segments for this message. + + This value is not contractual; the actual segment count will be determined after + the SMS is sent by the provider. Only present for SMS messages. + """ + from_: Optional[str] = FieldInfo(alias="from", default=None) """The Sender ID used for this message.""" From bc95591afd9e4e6e09a4c8f83a5730ad3a5ed051 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:45:06 +0000 Subject: [PATCH 15/34] feat(api): api update --- .stats.yml | 4 +- .../types/watch_send_feedbacks_params.py | 70 +------------------ 2 files changed, 3 insertions(+), 71 deletions(-) diff --git a/.stats.yml b/.stats.yml index d725f1b..418df5e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ae06b3bf0c78b49f67ba55dd910b013e4cd9b8c8b97896734354d8f233a4f5bb.yml -openapi_spec_hash: 129ee2e95ca86f1fbef1f61ca4599c66 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ccc75af90ed5716784f8993b6f0d7d15d58f2b6ae02633eecbe6858496dbaef5.yml +openapi_spec_hash: 700c9498f2eac1de427535080c7a0474 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py index c15e57d..595164c 100644 --- a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py +++ b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py @@ -5,7 +5,7 @@ from typing import Iterable from typing_extensions import Literal, Required, TypedDict -__all__ = ["WatchSendFeedbacksParams", "Feedback", "FeedbackTarget", "FeedbackMetadata", "FeedbackSignals"] +__all__ = ["WatchSendFeedbacksParams", "Feedback", "FeedbackTarget", "FeedbackMetadata"] class WatchSendFeedbacksParams(TypedDict, total=False): @@ -34,64 +34,6 @@ class FeedbackMetadata(TypedDict, total=False): """ -class FeedbackSignals(TypedDict, total=False): - """The signals used for anti-fraud. - - For more details, refer to [Signals](/verify/v2/documentation/prevent-fraud#signals). - """ - - app_version: str - """The version of your application.""" - - device_id: str - """A unique ID for the user's device. - - You should ensure that each user device has a unique `device_id` value. Ideally, - for Android, this corresponds to the `ANDROID_ID` and for iOS, this corresponds - to the `identifierForVendor`. - """ - - device_model: str - """The model of the user's device.""" - - device_platform: Literal["android", "ios", "ipados", "tvos", "web"] - """The type of the user's device.""" - - ip: str - """The public IP v4 or v6 address of the end-user's device. - - You should collect this from your backend. If your backend is behind a proxy, - use the `X-Forwarded-For`, `Forwarded`, `True-Client-IP`, `CF-Connecting-IP` or - an equivalent header to get the actual public IP of the end-user's device. - """ - - is_trusted_user: bool - """ - This signal should indicate a higher level of trust, explicitly stating that the - user is genuine. Contact us to discuss your use case. For more details, refer to - [Signals](/verify/v2/documentation/prevent-fraud#signals). - """ - - ja4_fingerprint: str - """The JA4 fingerprint observed for the end-user's connection. - - Prelude will infer it automatically when you use our Frontend SDKs (which use - Prelude's edge network), but you can also forward the value if you terminate TLS - yourself. - """ - - os_version: str - """The version of the user's device operating system.""" - - user_agent: str - """The user agent of the user's device. - - If the individual fields (os_version, device_platform, device_model) are - provided, we will prioritize those values instead of parsing them from the user - agent string. - """ - - class Feedback(TypedDict, total=False): target: Required[FeedbackTarget] """The feedback target. Only supports phone numbers for now.""" @@ -99,15 +41,5 @@ class Feedback(TypedDict, total=False): type: Required[Literal["verification.started", "verification.completed"]] """The type of feedback.""" - dispatch_id: str - """The identifier of the dispatch that came from the front-end SDK.""" - metadata: FeedbackMetadata """The metadata for this feedback.""" - - signals: FeedbackSignals - """The signals used for anti-fraud. - - For more details, refer to - [Signals](/verify/v2/documentation/prevent-fraud#signals). - """ From 24288cdaca292916fbb906003adcbb80f8315343 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:22:23 +0000 Subject: [PATCH 16/34] feat(api): api update --- .stats.yml | 4 ++-- src/prelude_python_sdk/types/verification_create_params.py | 3 --- tests/api_resources/test_verification.py | 2 -- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 418df5e..ae72acf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ccc75af90ed5716784f8993b6f0d7d15d58f2b6ae02633eecbe6858496dbaef5.yml -openapi_spec_hash: 700c9498f2eac1de427535080c7a0474 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-66392c85c3a3a624b974ea015fe24f09fde846fbf2758fa1a59386b7d0743324.yml +openapi_spec_hash: 1fd678e8896ab7a524a7181b348cb69a config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index bbd051c..152385f 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -116,9 +116,6 @@ class Options(TypedDict, total=False): more details, refer to [Custom Code](/verify/v2/documentation/custom-codes). """ - integration: Literal["auth0", "supabase"] - """The integration that triggered the verification.""" - locale: str """ A BCP-47 formatted locale string with the language the text message will be sent diff --git a/tests/api_resources/test_verification.py b/tests/api_resources/test_verification.py index c08cf6e..b7e390d 100644 --- a/tests/api_resources/test_verification.py +++ b/tests/api_resources/test_verification.py @@ -49,7 +49,6 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: "callback_url": "callback_url", "code_size": 5, "custom_code": "123456", - "integration": "auth0", "locale": "el-GR", "method": "auto", "preferred_channel": "sms", @@ -181,7 +180,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - "callback_url": "callback_url", "code_size": 5, "custom_code": "123456", - "integration": "auth0", "locale": "el-GR", "method": "auto", "preferred_channel": "sms", From 7b89b884402982ffc74e27f13bfccdff24b8ff75 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:57:03 +0000 Subject: [PATCH 17/34] feat(client): add custom JSON encoder for extended type support --- src/prelude_python_sdk/_base_client.py | 7 +- src/prelude_python_sdk/_compat.py | 6 +- src/prelude_python_sdk/_utils/_json.py | 35 +++++++ tests/test_utils/test_json.py | 126 +++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/prelude_python_sdk/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/prelude_python_sdk/_base_client.py b/src/prelude_python_sdk/_base_client.py index 83c50bb..c6309e4 100644 --- a/src/prelude_python_sdk/_base_client.py +++ b/src/prelude_python_sdk/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/prelude_python_sdk/_compat.py b/src/prelude_python_sdk/_compat.py index bdef67f..786ff42 100644 --- a/src/prelude_python_sdk/_compat.py +++ b/src/prelude_python_sdk/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/prelude_python_sdk/_utils/_json.py b/src/prelude_python_sdk/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/prelude_python_sdk/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..a6dd30c --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from prelude_python_sdk import _compat +from prelude_python_sdk._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From cd147cc674b99d1f42840dc8be4cbc50abe458cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:34:21 +0000 Subject: [PATCH 18/34] chore(internal): bump dependencies --- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 10e306b..c6bb79b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via prelude-python-sdk aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via prelude-python-sdk argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via httpx-aiohttp # via prelude-python-sdk # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via prelude-python-sdk humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via prelude-python-sdk time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via virtualenv typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index 39e3f94..6bc76f8 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via httpx-aiohttp # via prelude-python-sdk aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via httpx # via prelude-python-sdk async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via prelude-python-sdk -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via prelude-python-sdk idna==3.11 # via anyio From 6fa1e9a65dd600399a33c4184b159693dc5f6fd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:20:45 +0000 Subject: [PATCH 19/34] chore(internal): fix lint error on Python 3.14 --- src/prelude_python_sdk/_utils/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prelude_python_sdk/_utils/_compat.py b/src/prelude_python_sdk/_utils/_compat.py index dd70323..2c70b29 100644 --- a/src/prelude_python_sdk/_utils/_compat.py +++ b/src/prelude_python_sdk/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From cafa350bb5651faa97e3d5b0ba520a24805e57ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:52:25 +0000 Subject: [PATCH 20/34] chore: format all `api.md` files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 337da74..01cdbbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ From 4c5c4e253a4f490b338d5536201a0ae3d5bc64a7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:10:04 +0000 Subject: [PATCH 21/34] feat(api): api update --- .stats.yml | 4 ++-- src/prelude_python_sdk/resources/notify.py | 20 ++++++++++++++++++ .../resources/transactional.py | 10 +++++++++ .../types/notify_send_batch_params.py | 21 ++++++++++++++++++- .../types/notify_send_params.py | 21 ++++++++++++++++++- .../types/transactional_send_params.py | 21 ++++++++++++++++++- tests/api_resources/test_notify.py | 16 ++++++++++++++ tests/api_resources/test_transactional.py | 8 +++++++ 8 files changed, 116 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index ae72acf..d3e30c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-66392c85c3a3a624b974ea015fe24f09fde846fbf2758fa1a59386b7d0743324.yml -openapi_spec_hash: 1fd678e8896ab7a524a7181b348cb69a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml +openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 config_hash: 55380048fe5cf686421acf7eeaae21be diff --git a/src/prelude_python_sdk/resources/notify.py b/src/prelude_python_sdk/resources/notify.py index 70d2366..e9f36c7 100644 --- a/src/prelude_python_sdk/resources/notify.py +++ b/src/prelude_python_sdk/resources/notify.py @@ -297,6 +297,7 @@ def send( to: str, callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: notify_send_params.Document | Omit = omit, expires_at: Union[str, datetime] | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -325,6 +326,9 @@ def send( It is returned in the response and any webhook events that refer to this message. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date in RFC3339 format. The message will not be sent if this time is reached. @@ -360,6 +364,7 @@ def send( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, @@ -382,6 +387,7 @@ def send_batch( to: SequenceNotStr[str], callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: notify_send_batch_params.Document | Omit = omit, expires_at: Union[str, datetime] | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -407,6 +413,9 @@ def send_batch( correlation_id: A user-defined identifier to correlate this request with your internal systems. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date in RFC3339 format. Messages will not be sent after this time. @@ -437,6 +446,7 @@ def send_batch( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, @@ -711,6 +721,7 @@ async def send( to: str, callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: notify_send_params.Document | Omit = omit, expires_at: Union[str, datetime] | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -739,6 +750,9 @@ async def send( It is returned in the response and any webhook events that refer to this message. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date in RFC3339 format. The message will not be sent if this time is reached. @@ -774,6 +788,7 @@ async def send( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, @@ -796,6 +811,7 @@ async def send_batch( to: SequenceNotStr[str], callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: notify_send_batch_params.Document | Omit = omit, expires_at: Union[str, datetime] | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -821,6 +837,9 @@ async def send_batch( correlation_id: A user-defined identifier to correlate this request with your internal systems. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date in RFC3339 format. Messages will not be sent after this time. @@ -851,6 +870,7 @@ async def send_batch( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, diff --git a/src/prelude_python_sdk/resources/transactional.py b/src/prelude_python_sdk/resources/transactional.py index 150e6ad..90cb8ee 100644 --- a/src/prelude_python_sdk/resources/transactional.py +++ b/src/prelude_python_sdk/resources/transactional.py @@ -53,6 +53,7 @@ def send( to: str, callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: transactional_send_params.Document | Omit = omit, expires_at: str | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -81,6 +82,9 @@ def send( returned in the response and any webhook events that refer to this transactionalmessage. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date. from_: The Sender ID. @@ -118,6 +122,7 @@ def send( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, @@ -161,6 +166,7 @@ async def send( to: str, callback_url: str | Omit = omit, correlation_id: str | Omit = omit, + document: transactional_send_params.Document | Omit = omit, expires_at: str | Omit = omit, from_: str | Omit = omit, locale: str | Omit = omit, @@ -189,6 +195,9 @@ async def send( returned in the response and any webhook events that refer to this transactionalmessage. + document: A document to attach to the message. Only supported on WhatsApp templates that + have a document header. + expires_at: The message expiration date. from_: The Sender ID. @@ -226,6 +235,7 @@ async def send( "to": to, "callback_url": callback_url, "correlation_id": correlation_id, + "document": document, "expires_at": expires_at, "from_": from_, "locale": locale, diff --git a/src/prelude_python_sdk/types/notify_send_batch_params.py b/src/prelude_python_sdk/types/notify_send_batch_params.py index c38b667..cf0398f 100644 --- a/src/prelude_python_sdk/types/notify_send_batch_params.py +++ b/src/prelude_python_sdk/types/notify_send_batch_params.py @@ -9,7 +9,7 @@ from .._types import SequenceNotStr from .._utils import PropertyInfo -__all__ = ["NotifySendBatchParams"] +__all__ = ["NotifySendBatchParams", "Document"] class NotifySendBatchParams(TypedDict, total=False): @@ -25,6 +25,12 @@ class NotifySendBatchParams(TypedDict, total=False): correlation_id: str """A user-defined identifier to correlate this request with your internal systems.""" + document: Document + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + expires_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] """The message expiration date in RFC3339 format. @@ -48,3 +54,16 @@ class NotifySendBatchParams(TypedDict, total=False): variables: Dict[str, str] """The variables to be replaced in the template.""" + + +class Document(TypedDict, total=False): + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + + filename: Required[str] + """The filename to display for the document.""" + + url: Required[str] + """The URL of the document to attach. Must be a valid HTTP or HTTPS URL.""" diff --git a/src/prelude_python_sdk/types/notify_send_params.py b/src/prelude_python_sdk/types/notify_send_params.py index afc2f9d..ff79d80 100644 --- a/src/prelude_python_sdk/types/notify_send_params.py +++ b/src/prelude_python_sdk/types/notify_send_params.py @@ -8,7 +8,7 @@ from .._utils import PropertyInfo -__all__ = ["NotifySendParams"] +__all__ = ["NotifySendParams", "Document"] class NotifySendParams(TypedDict, total=False): @@ -28,6 +28,12 @@ class NotifySendParams(TypedDict, total=False): message. """ + document: Document + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + expires_at: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] """The message expiration date in RFC3339 format. @@ -61,3 +67,16 @@ class NotifySendParams(TypedDict, total=False): variables: Dict[str, str] """The variables to be replaced in the template.""" + + +class Document(TypedDict, total=False): + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + + filename: Required[str] + """The filename to display for the document.""" + + url: Required[str] + """The URL of the document to attach. Must be a valid HTTP or HTTPS URL.""" diff --git a/src/prelude_python_sdk/types/transactional_send_params.py b/src/prelude_python_sdk/types/transactional_send_params.py index 97f176b..00f19a1 100644 --- a/src/prelude_python_sdk/types/transactional_send_params.py +++ b/src/prelude_python_sdk/types/transactional_send_params.py @@ -7,7 +7,7 @@ from .._utils import PropertyInfo -__all__ = ["TransactionalSendParams"] +__all__ = ["TransactionalSendParams", "Document"] class TransactionalSendParams(TypedDict, total=False): @@ -27,6 +27,12 @@ class TransactionalSendParams(TypedDict, total=False): transactionalmessage. """ + document: Document + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + expires_at: str """The message expiration date.""" @@ -56,3 +62,16 @@ class TransactionalSendParams(TypedDict, total=False): variables: Dict[str, str] """The variables to be replaced in the template.""" + + +class Document(TypedDict, total=False): + """A document to attach to the message. + + Only supported on WhatsApp templates that have a document header. + """ + + filename: Required[str] + """The filename to display for the document.""" + + url: Required[str] + """The URL of the document to attach. Must be a valid HTTP or HTTPS URL.""" diff --git a/tests/api_resources/test_notify.py b/tests/api_resources/test_notify.py index 67db153..ac0837d 100644 --- a/tests/api_resources/test_notify.py +++ b/tests/api_resources/test_notify.py @@ -268,6 +268,10 @@ def test_method_send_with_all_params(self, client: Prelude) -> None: to="+33612345678", callback_url="https://your-app.com/webhooks/notify", correlation_id="order-12345", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at=parse_datetime("2025-12-25T18:00:00Z"), from_="from", locale="el-GR", @@ -323,6 +327,10 @@ def test_method_send_batch_with_all_params(self, client: Prelude) -> None: to=["+33612345678", "+15551234567"], callback_url="https://your-app.com/webhooks/notify", correlation_id="campaign-12345", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at=parse_datetime("2025-12-25T18:00:00Z"), from_="from", locale="el-GR", @@ -611,6 +619,10 @@ async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> to="+33612345678", callback_url="https://your-app.com/webhooks/notify", correlation_id="order-12345", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at=parse_datetime("2025-12-25T18:00:00Z"), from_="from", locale="el-GR", @@ -666,6 +678,10 @@ async def test_method_send_batch_with_all_params(self, async_client: AsyncPrelud to=["+33612345678", "+15551234567"], callback_url="https://your-app.com/webhooks/notify", correlation_id="campaign-12345", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at=parse_datetime("2025-12-25T18:00:00Z"), from_="from", locale="el-GR", diff --git a/tests/api_resources/test_transactional.py b/tests/api_resources/test_transactional.py index 77ee4b1..6bd6144 100644 --- a/tests/api_resources/test_transactional.py +++ b/tests/api_resources/test_transactional.py @@ -39,6 +39,10 @@ def test_method_send_with_all_params(self, client: Prelude) -> None: to="+30123456789", callback_url="callback_url", correlation_id="correlation_id", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at="expires_at", from_="from", locale="el-GR", @@ -104,6 +108,10 @@ async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> to="+30123456789", callback_url="callback_url", correlation_id="correlation_id", + document={ + "filename": "invoice.pdf", + "url": "https://example.com/invoice.pdf", + }, expires_at="expires_at", from_="from", locale="el-GR", From b01c5bb68c17c9f1579ce0c52d7ab2b8c0745f20 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:14:10 +0000 Subject: [PATCH 22/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d3e30c8..ef634fe 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 -config_hash: 55380048fe5cf686421acf7eeaae21be +config_hash: 3f4b4b146cf4d7c7515abcdf87c49c38 From dcbbc25a7f52ab51c3a9b6f0c66687333769da1d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:16:02 +0000 Subject: [PATCH 23/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ef634fe..1b2837f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 -config_hash: 3f4b4b146cf4d7c7515abcdf87c49c38 +config_hash: 60967b027916003609d7fe879c9c7e64 From 99d100e717395125fb786947a6dd8a4e96ba93ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:18:08 +0000 Subject: [PATCH 24/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1b2837f..2a5a3b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 -config_hash: 60967b027916003609d7fe879c9c7e64 +config_hash: e3126baa0fd4be7b985bb956faa49011 From 76efbc2329f29eb501e6f27c0ba2e966e3278ced Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:19:46 +0000 Subject: [PATCH 25/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2a5a3b9..07efe66 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 -config_hash: e3126baa0fd4be7b985bb956faa49011 +config_hash: 107ae5754168e80c4ad2cd779a75bc36 From 85a5da01c2f04110317bd8a8c5a4cec4bd7be831 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:32:32 +0000 Subject: [PATCH 26/34] chore: update mock server docs --- CONTRIBUTING.md | 3 +-- tests/api_resources/test_notify.py | 16 ++++++++-------- tests/api_resources/test_transactional.py | 16 ++++++++-------- tests/api_resources/test_verification.py | 16 ++++++++-------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d42531..d7fbe4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. ```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh diff --git a/tests/api_resources/test_notify.py b/tests/api_resources/test_notify.py index ac0837d..a85e95d 100644 --- a/tests/api_resources/test_notify.py +++ b/tests/api_resources/test_notify.py @@ -251,7 +251,7 @@ def test_path_params_list_subscription_phone_numbers(self, client: Prelude) -> N config_id="", ) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_send(self, client: Prelude) -> None: notify = client.notify.send( @@ -260,7 +260,7 @@ def test_method_send(self, client: Prelude) -> None: ) assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_send_with_all_params(self, client: Prelude) -> None: notify = client.notify.send( @@ -284,7 +284,7 @@ def test_method_send_with_all_params(self, client: Prelude) -> None: ) assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_raw_response_send(self, client: Prelude) -> None: response = client.notify.with_raw_response.send( @@ -297,7 +297,7 @@ def test_raw_response_send(self, client: Prelude) -> None: notify = response.parse() assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_streaming_response_send(self, client: Prelude) -> None: with client.notify.with_streaming_response.send( @@ -602,7 +602,7 @@ async def test_path_params_list_subscription_phone_numbers(self, async_client: A config_id="", ) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_send(self, async_client: AsyncPrelude) -> None: notify = await async_client.notify.send( @@ -611,7 +611,7 @@ async def test_method_send(self, async_client: AsyncPrelude) -> None: ) assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> None: notify = await async_client.notify.send( @@ -635,7 +635,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> ) assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_raw_response_send(self, async_client: AsyncPrelude) -> None: response = await async_client.notify.with_raw_response.send( @@ -648,7 +648,7 @@ async def test_raw_response_send(self, async_client: AsyncPrelude) -> None: notify = await response.parse() assert_matches_type(NotifySendResponse, notify, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_streaming_response_send(self, async_client: AsyncPrelude) -> None: async with async_client.notify.with_streaming_response.send( diff --git a/tests/api_resources/test_transactional.py b/tests/api_resources/test_transactional.py index 6bd6144..416b235 100644 --- a/tests/api_resources/test_transactional.py +++ b/tests/api_resources/test_transactional.py @@ -19,7 +19,7 @@ class TestTransactional: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_send(self, client: Prelude) -> None: with pytest.warns(DeprecationWarning): @@ -30,7 +30,7 @@ def test_method_send(self, client: Prelude) -> None: assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_send_with_all_params(self, client: Prelude) -> None: with pytest.warns(DeprecationWarning): @@ -52,7 +52,7 @@ def test_method_send_with_all_params(self, client: Prelude) -> None: assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_raw_response_send(self, client: Prelude) -> None: with pytest.warns(DeprecationWarning): @@ -66,7 +66,7 @@ def test_raw_response_send(self, client: Prelude) -> None: transactional = response.parse() assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_streaming_response_send(self, client: Prelude) -> None: with pytest.warns(DeprecationWarning): @@ -88,7 +88,7 @@ class TestAsyncTransactional: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_send(self, async_client: AsyncPrelude) -> None: with pytest.warns(DeprecationWarning): @@ -99,7 +99,7 @@ async def test_method_send(self, async_client: AsyncPrelude) -> None: assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> None: with pytest.warns(DeprecationWarning): @@ -121,7 +121,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncPrelude) -> assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_raw_response_send(self, async_client: AsyncPrelude) -> None: with pytest.warns(DeprecationWarning): @@ -135,7 +135,7 @@ async def test_raw_response_send(self, async_client: AsyncPrelude) -> None: transactional = await response.parse() assert_matches_type(TransactionalSendResponse, transactional, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_streaming_response_send(self, async_client: AsyncPrelude) -> None: with pytest.warns(DeprecationWarning): diff --git a/tests/api_resources/test_verification.py b/tests/api_resources/test_verification.py index b7e390d..b2eaa31 100644 --- a/tests/api_resources/test_verification.py +++ b/tests/api_resources/test_verification.py @@ -20,7 +20,7 @@ class TestVerification: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_create(self, client: Prelude) -> None: verification = client.verification.create( @@ -31,7 +31,7 @@ def test_method_create(self, client: Prelude) -> None: ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_method_create_with_all_params(self, client: Prelude) -> None: verification = client.verification.create( @@ -70,7 +70,7 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_raw_response_create(self, client: Prelude) -> None: response = client.verification.with_raw_response.create( @@ -85,7 +85,7 @@ def test_raw_response_create(self, client: Prelude) -> None: verification = response.parse() assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize def test_streaming_response_create(self, client: Prelude) -> None: with client.verification.with_streaming_response.create( @@ -151,7 +151,7 @@ class TestAsyncVerification: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_create(self, async_client: AsyncPrelude) -> None: verification = await async_client.verification.create( @@ -162,7 +162,7 @@ async def test_method_create(self, async_client: AsyncPrelude) -> None: ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncPrelude) -> None: verification = await async_client.verification.create( @@ -201,7 +201,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_raw_response_create(self, async_client: AsyncPrelude) -> None: response = await async_client.verification.with_raw_response.create( @@ -216,7 +216,7 @@ async def test_raw_response_create(self, async_client: AsyncPrelude) -> None: verification = await response.parse() assert_matches_type(VerificationCreateResponse, verification, path=["response"]) - @pytest.mark.skip(reason="Prism doesn't support callbacks yet") + @pytest.mark.skip(reason="Mock server doesn't support callbacks yet") @parametrize async def test_streaming_response_create(self, async_client: AsyncPrelude) -> None: async with async_client.verification.with_streaming_response.create( From 1d2f5baf54ac0ed2e95c94c0c8a5ad97456d8db7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:37:06 +0000 Subject: [PATCH 27/34] chore(internal): add request options to SSE classes --- src/prelude_python_sdk/_response.py | 3 +++ src/prelude_python_sdk/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/prelude_python_sdk/_response.py b/src/prelude_python_sdk/_response.py index e6e260d..f8c8d96 100644 --- a/src/prelude_python_sdk/_response.py +++ b/src/prelude_python_sdk/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/prelude_python_sdk/_streaming.py b/src/prelude_python_sdk/_streaming.py index 13f497a..0be8b8c 100644 --- a/src/prelude_python_sdk/_streaming.py +++ b/src/prelude_python_sdk/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Prelude, AsyncPrelude + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Prelude, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncPrelude, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 1ca0a6ec1b01fc52bf7c02225a4a63873da0a75b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:26:33 +0000 Subject: [PATCH 28/34] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index ed835f2..6eb5e81 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -987,6 +987,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1921,6 +1923,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From 0e5b065a92f740de2a59ad94777ca4890b0aba13 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:41:17 +0000 Subject: [PATCH 29/34] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 6eb5e81..8d9049a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -987,8 +987,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1923,8 +1929,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 1a73fd80ad7b60fda2da8ad9746aede6b7d7bf1c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:02:17 +0000 Subject: [PATCH 30/34] chore(docs): add missing descriptions --- src/prelude_python_sdk/_client.py | 48 +++++++++++++++++++ src/prelude_python_sdk/resources/lookup.py | 8 ++++ src/prelude_python_sdk/resources/notify.py | 4 ++ .../resources/transactional.py | 4 ++ .../resources/verification.py | 4 ++ .../resources/verification_management.py | 4 ++ src/prelude_python_sdk/resources/watch.py | 4 ++ 7 files changed, 76 insertions(+) diff --git a/src/prelude_python_sdk/_client.py b/src/prelude_python_sdk/_client.py index a212479..17e918c 100644 --- a/src/prelude_python_sdk/_client.py +++ b/src/prelude_python_sdk/_client.py @@ -99,36 +99,44 @@ def __init__( @cached_property def lookup(self) -> LookupResource: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import LookupResource return LookupResource(self) @cached_property def notify(self) -> NotifyResource: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import NotifyResource return NotifyResource(self) @cached_property def transactional(self) -> TransactionalResource: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import TransactionalResource return TransactionalResource(self) @cached_property def verification(self) -> VerificationResource: + """Verify phone numbers.""" from .resources.verification import VerificationResource return VerificationResource(self) @cached_property def verification_management(self) -> VerificationManagementResource: + """Verify phone numbers.""" from .resources.verification_management import VerificationManagementResource return VerificationManagementResource(self) @cached_property def watch(self) -> WatchResource: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import WatchResource return WatchResource(self) @@ -303,36 +311,44 @@ def __init__( @cached_property def lookup(self) -> AsyncLookupResource: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import AsyncLookupResource return AsyncLookupResource(self) @cached_property def notify(self) -> AsyncNotifyResource: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import AsyncNotifyResource return AsyncNotifyResource(self) @cached_property def transactional(self) -> AsyncTransactionalResource: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import AsyncTransactionalResource return AsyncTransactionalResource(self) @cached_property def verification(self) -> AsyncVerificationResource: + """Verify phone numbers.""" from .resources.verification import AsyncVerificationResource return AsyncVerificationResource(self) @cached_property def verification_management(self) -> AsyncVerificationManagementResource: + """Verify phone numbers.""" from .resources.verification_management import AsyncVerificationManagementResource return AsyncVerificationManagementResource(self) @cached_property def watch(self) -> AsyncWatchResource: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import AsyncWatchResource return AsyncWatchResource(self) @@ -458,36 +474,44 @@ def __init__(self, client: Prelude) -> None: @cached_property def lookup(self) -> lookup.LookupResourceWithRawResponse: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import LookupResourceWithRawResponse return LookupResourceWithRawResponse(self._client.lookup) @cached_property def notify(self) -> notify.NotifyResourceWithRawResponse: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import NotifyResourceWithRawResponse return NotifyResourceWithRawResponse(self._client.notify) @cached_property def transactional(self) -> transactional.TransactionalResourceWithRawResponse: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import TransactionalResourceWithRawResponse return TransactionalResourceWithRawResponse(self._client.transactional) @cached_property def verification(self) -> verification.VerificationResourceWithRawResponse: + """Verify phone numbers.""" from .resources.verification import VerificationResourceWithRawResponse return VerificationResourceWithRawResponse(self._client.verification) @cached_property def verification_management(self) -> verification_management.VerificationManagementResourceWithRawResponse: + """Verify phone numbers.""" from .resources.verification_management import VerificationManagementResourceWithRawResponse return VerificationManagementResourceWithRawResponse(self._client.verification_management) @cached_property def watch(self) -> watch.WatchResourceWithRawResponse: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import WatchResourceWithRawResponse return WatchResourceWithRawResponse(self._client.watch) @@ -501,36 +525,44 @@ def __init__(self, client: AsyncPrelude) -> None: @cached_property def lookup(self) -> lookup.AsyncLookupResourceWithRawResponse: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import AsyncLookupResourceWithRawResponse return AsyncLookupResourceWithRawResponse(self._client.lookup) @cached_property def notify(self) -> notify.AsyncNotifyResourceWithRawResponse: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import AsyncNotifyResourceWithRawResponse return AsyncNotifyResourceWithRawResponse(self._client.notify) @cached_property def transactional(self) -> transactional.AsyncTransactionalResourceWithRawResponse: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import AsyncTransactionalResourceWithRawResponse return AsyncTransactionalResourceWithRawResponse(self._client.transactional) @cached_property def verification(self) -> verification.AsyncVerificationResourceWithRawResponse: + """Verify phone numbers.""" from .resources.verification import AsyncVerificationResourceWithRawResponse return AsyncVerificationResourceWithRawResponse(self._client.verification) @cached_property def verification_management(self) -> verification_management.AsyncVerificationManagementResourceWithRawResponse: + """Verify phone numbers.""" from .resources.verification_management import AsyncVerificationManagementResourceWithRawResponse return AsyncVerificationManagementResourceWithRawResponse(self._client.verification_management) @cached_property def watch(self) -> watch.AsyncWatchResourceWithRawResponse: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import AsyncWatchResourceWithRawResponse return AsyncWatchResourceWithRawResponse(self._client.watch) @@ -544,36 +576,44 @@ def __init__(self, client: Prelude) -> None: @cached_property def lookup(self) -> lookup.LookupResourceWithStreamingResponse: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import LookupResourceWithStreamingResponse return LookupResourceWithStreamingResponse(self._client.lookup) @cached_property def notify(self) -> notify.NotifyResourceWithStreamingResponse: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import NotifyResourceWithStreamingResponse return NotifyResourceWithStreamingResponse(self._client.notify) @cached_property def transactional(self) -> transactional.TransactionalResourceWithStreamingResponse: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import TransactionalResourceWithStreamingResponse return TransactionalResourceWithStreamingResponse(self._client.transactional) @cached_property def verification(self) -> verification.VerificationResourceWithStreamingResponse: + """Verify phone numbers.""" from .resources.verification import VerificationResourceWithStreamingResponse return VerificationResourceWithStreamingResponse(self._client.verification) @cached_property def verification_management(self) -> verification_management.VerificationManagementResourceWithStreamingResponse: + """Verify phone numbers.""" from .resources.verification_management import VerificationManagementResourceWithStreamingResponse return VerificationManagementResourceWithStreamingResponse(self._client.verification_management) @cached_property def watch(self) -> watch.WatchResourceWithStreamingResponse: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import WatchResourceWithStreamingResponse return WatchResourceWithStreamingResponse(self._client.watch) @@ -587,24 +627,30 @@ def __init__(self, client: AsyncPrelude) -> None: @cached_property def lookup(self) -> lookup.AsyncLookupResourceWithStreamingResponse: + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ from .resources.lookup import AsyncLookupResourceWithStreamingResponse return AsyncLookupResourceWithStreamingResponse(self._client.lookup) @cached_property def notify(self) -> notify.AsyncNotifyResourceWithStreamingResponse: + """Send transactional and marketing messages with compliance enforcement.""" from .resources.notify import AsyncNotifyResourceWithStreamingResponse return AsyncNotifyResourceWithStreamingResponse(self._client.notify) @cached_property def transactional(self) -> transactional.AsyncTransactionalResourceWithStreamingResponse: + """Send transactional messages (deprecated - use Notify API instead).""" from .resources.transactional import AsyncTransactionalResourceWithStreamingResponse return AsyncTransactionalResourceWithStreamingResponse(self._client.transactional) @cached_property def verification(self) -> verification.AsyncVerificationResourceWithStreamingResponse: + """Verify phone numbers.""" from .resources.verification import AsyncVerificationResourceWithStreamingResponse return AsyncVerificationResourceWithStreamingResponse(self._client.verification) @@ -613,12 +659,14 @@ def verification(self) -> verification.AsyncVerificationResourceWithStreamingRes def verification_management( self, ) -> verification_management.AsyncVerificationManagementResourceWithStreamingResponse: + """Verify phone numbers.""" from .resources.verification_management import AsyncVerificationManagementResourceWithStreamingResponse return AsyncVerificationManagementResourceWithStreamingResponse(self._client.verification_management) @cached_property def watch(self) -> watch.AsyncWatchResourceWithStreamingResponse: + """Evaluate email addresses and phone numbers for trustworthiness.""" from .resources.watch import AsyncWatchResourceWithStreamingResponse return AsyncWatchResourceWithStreamingResponse(self._client.watch) diff --git a/src/prelude_python_sdk/resources/lookup.py b/src/prelude_python_sdk/resources/lookup.py index a876d32..7f29a9e 100644 --- a/src/prelude_python_sdk/resources/lookup.py +++ b/src/prelude_python_sdk/resources/lookup.py @@ -25,6 +25,10 @@ class LookupResource(SyncAPIResource): + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ + @cached_property def with_raw_response(self) -> LookupResourceWithRawResponse: """ @@ -93,6 +97,10 @@ def lookup( class AsyncLookupResource(AsyncAPIResource): + """ + Retrieve detailed information about a phone number including carrier data, line type, and portability status. + """ + @cached_property def with_raw_response(self) -> AsyncLookupResourceWithRawResponse: """ diff --git a/src/prelude_python_sdk/resources/notify.py b/src/prelude_python_sdk/resources/notify.py index e9f36c7..f810768 100644 --- a/src/prelude_python_sdk/resources/notify.py +++ b/src/prelude_python_sdk/resources/notify.py @@ -40,6 +40,8 @@ class NotifyResource(SyncAPIResource): + """Send transactional and marketing messages with compliance enforcement.""" + @cached_property def with_raw_response(self) -> NotifyResourceWithRawResponse: """ @@ -464,6 +466,8 @@ def send_batch( class AsyncNotifyResource(AsyncAPIResource): + """Send transactional and marketing messages with compliance enforcement.""" + @cached_property def with_raw_response(self) -> AsyncNotifyResourceWithRawResponse: """ diff --git a/src/prelude_python_sdk/resources/transactional.py b/src/prelude_python_sdk/resources/transactional.py index 90cb8ee..562c053 100644 --- a/src/prelude_python_sdk/resources/transactional.py +++ b/src/prelude_python_sdk/resources/transactional.py @@ -26,6 +26,8 @@ class TransactionalResource(SyncAPIResource): + """Send transactional messages (deprecated - use Notify API instead).""" + @cached_property def with_raw_response(self) -> TransactionalResourceWithRawResponse: """ @@ -139,6 +141,8 @@ def send( class AsyncTransactionalResource(AsyncAPIResource): + """Send transactional messages (deprecated - use Notify API instead).""" + @cached_property def with_raw_response(self) -> AsyncTransactionalResourceWithRawResponse: """ diff --git a/src/prelude_python_sdk/resources/verification.py b/src/prelude_python_sdk/resources/verification.py index 7b47cd2..d86bca8 100644 --- a/src/prelude_python_sdk/resources/verification.py +++ b/src/prelude_python_sdk/resources/verification.py @@ -23,6 +23,8 @@ class VerificationResource(SyncAPIResource): + """Verify phone numbers.""" + @cached_property def with_raw_response(self) -> VerificationResourceWithRawResponse: """ @@ -149,6 +151,8 @@ def check( class AsyncVerificationResource(AsyncAPIResource): + """Verify phone numbers.""" + @cached_property def with_raw_response(self) -> AsyncVerificationResourceWithRawResponse: """ diff --git a/src/prelude_python_sdk/resources/verification_management.py b/src/prelude_python_sdk/resources/verification_management.py index b9f0b61..a682543 100644 --- a/src/prelude_python_sdk/resources/verification_management.py +++ b/src/prelude_python_sdk/resources/verification_management.py @@ -32,6 +32,8 @@ class VerificationManagementResource(SyncAPIResource): + """Verify phone numbers.""" + @cached_property def with_raw_response(self) -> VerificationManagementResourceWithRawResponse: """ @@ -242,6 +244,8 @@ def submit_sender_id( class AsyncVerificationManagementResource(AsyncAPIResource): + """Verify phone numbers.""" + @cached_property def with_raw_response(self) -> AsyncVerificationManagementResourceWithRawResponse: """ diff --git a/src/prelude_python_sdk/resources/watch.py b/src/prelude_python_sdk/resources/watch.py index ef12237..cdce1e5 100644 --- a/src/prelude_python_sdk/resources/watch.py +++ b/src/prelude_python_sdk/resources/watch.py @@ -26,6 +26,8 @@ class WatchResource(SyncAPIResource): + """Evaluate email addresses and phone numbers for trustworthiness.""" + @cached_property def with_raw_response(self) -> WatchResourceWithRawResponse: """ @@ -170,6 +172,8 @@ def send_feedbacks( class AsyncWatchResource(AsyncAPIResource): + """Evaluate email addresses and phone numbers for trustworthiness.""" + @cached_property def with_raw_response(self) -> AsyncWatchResourceWithRawResponse: """ From 859b8f0cccf997852bc2d1911b1699da74741551 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:49:57 +0000 Subject: [PATCH 31/34] feat(api): api update --- .stats.yml | 4 ++-- .../types/verification_create_response.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 07efe66..d3333fd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 19 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ff647628bff6dbf261d7f1edd1ffdf89c168d9c964d1afe118a130d4074b00ac.yml -openapi_spec_hash: 92dbcb767fa8e1a82638d1831a9550d7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-c9124c257dd54dd0728cb57306b9082007439bfefac11642542605edb3e7606d.yml +openapi_spec_hash: 61dc64cc814d10975a8825ec88fd9c1c config_hash: 107ae5754168e80c4ad2cd779a75bc36 diff --git a/src/prelude_python_sdk/types/verification_create_response.py b/src/prelude_python_sdk/types/verification_create_response.py index 08cfbe7..f187077 100644 --- a/src/prelude_python_sdk/types/verification_create_response.py +++ b/src/prelude_python_sdk/types/verification_create_response.py @@ -33,8 +33,16 @@ class VerificationCreateResponse(BaseModel): method: Literal["email", "message", "silent", "voice"] """The method used for verifying this phone number.""" - status: Literal["success", "retry", "blocked"] - """The status of the verification.""" + status: Literal["success", "retry", "challenged", "blocked"] + """The status of the verification. + + - `success` - A new verification window was created. + - `retry` - A new attempt was created for an existing verification window. + - `challenged` - The verification is suspicious and is restricted to non-SMS and + non-voice channels only. This mode must be enabled for your customer account + by Prelude support. + - `blocked` - The verification was blocked. + """ channels: Optional[List[Literal["rcs", "silent", "sms", "telegram", "viber", "voice", "whatsapp", "zalo"]]] = None """The ordered sequence of channels to be used for verification""" From 73aaccf6b2dde8d80b4ee5d090a9057f847b4bac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:46:40 +0000 Subject: [PATCH 32/34] chore(test): do not count install time for mock server timeout --- scripts/mock | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/mock b/scripts/mock index 0b28f6e..bcf3b39 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done From c6b360343d0e2115be558ae6b17017235428206d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:56:37 +0000 Subject: [PATCH 33/34] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6a4815..d1853c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/prelude-python' + if: |- + github.repository == 'stainless-sdks/prelude-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/prelude-python' + if: |- + github.repository == 'stainless-sdks/prelude-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From fb6254322916262969a4a7d773867d02fdb167d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:57:00 +0000 Subject: [PATCH 34/34] release: 0.11.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/prelude_python_sdk/_version.py | 2 +- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 091cfb1..f7014c3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.0" + ".": "0.11.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e11a1..8d8b0b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 0.11.0 (2026-03-07) + +Full Changelog: [v0.10.0...v0.11.0](https://github.com/prelude-so/python-sdk/compare/v0.10.0...v0.11.0) + +### Features + +* **api:** api update ([859b8f0](https://github.com/prelude-so/python-sdk/commit/859b8f0cccf997852bc2d1911b1699da74741551)) +* **api:** api update ([4c5c4e2](https://github.com/prelude-so/python-sdk/commit/4c5c4e253a4f490b338d5536201a0ae3d5bc64a7)) +* **api:** api update ([24288cd](https://github.com/prelude-so/python-sdk/commit/24288cdaca292916fbb906003adcbb80f8315343)) +* **api:** api update ([bc95591](https://github.com/prelude-so/python-sdk/commit/bc95591afd9e4e6e09a4c8f83a5730ad3a5ed051)) +* **api:** api update ([c00e5f5](https://github.com/prelude-so/python-sdk/commit/c00e5f5f604a94604140731637d7fb975376188e)) +* **api:** api update ([f7afece](https://github.com/prelude-so/python-sdk/commit/f7afece5c9ea5bb3f3420a8a1f543da1e136bcff)) +* **api:** api update ([94f4fa9](https://github.com/prelude-so/python-sdk/commit/94f4fa9904573e950ae6d01d289cce34e9e70370)) +* **api:** api update ([6ab992c](https://github.com/prelude-so/python-sdk/commit/6ab992cc4da735868c0aed05cd737ed33f2c8d68)) +* **client:** add custom JSON encoder for extended type support ([7b89b88](https://github.com/prelude-so/python-sdk/commit/7b89b884402982ffc74e27f13bfccdff24b8ff75)) +* **client:** add support for binary request streaming ([5cc25b3](https://github.com/prelude-so/python-sdk/commit/5cc25b3416e9fe458d27fb26a0f8ebca1c36312a)) + + +### Bug Fixes + +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([1197a12](https://github.com/prelude-so/python-sdk/commit/1197a1218644530a19719ab9866385a3155581d5)) +* use async_to_httpx_files in patch method ([2a4c51a](https://github.com/prelude-so/python-sdk/commit/2a4c51aa40b31c9709fd482771a5eb1defd60786)) + + +### Chores + +* add missing docstrings ([6fc2141](https://github.com/prelude-so/python-sdk/commit/6fc2141b36efba6d357336a36c1305864ffec8cb)) +* **ci:** skip uploading artifacts on stainless-internal branches ([c6b3603](https://github.com/prelude-so/python-sdk/commit/c6b360343d0e2115be558ae6b17017235428206d)) +* **ci:** upgrade `actions/github-script` ([37e674a](https://github.com/prelude-so/python-sdk/commit/37e674a17a76e2a0bb3a2f9b29439ea233830109)) +* **docs:** add missing descriptions ([1a73fd8](https://github.com/prelude-so/python-sdk/commit/1a73fd80ad7b60fda2da8ad9746aede6b7d7bf1c)) +* format all `api.md` files ([cafa350](https://github.com/prelude-so/python-sdk/commit/cafa350bb5651faa97e3d5b0ba520a24805e57ae)) +* **internal:** add `--fix` argument to lint script ([aaa77d7](https://github.com/prelude-so/python-sdk/commit/aaa77d73edce4cc03a4cd203b90a7c6f9e145bbd)) +* **internal:** add missing files argument to base client ([84ade2c](https://github.com/prelude-so/python-sdk/commit/84ade2c96740cc217646929248a0b4e60c2434f0)) +* **internal:** add request options to SSE classes ([1d2f5ba](https://github.com/prelude-so/python-sdk/commit/1d2f5baf54ac0ed2e95c94c0c8a5ad97456d8db7)) +* **internal:** bump dependencies ([cd147cc](https://github.com/prelude-so/python-sdk/commit/cd147cc674b99d1f42840dc8be4cbc50abe458cb)) +* **internal:** codegen related update ([72f7709](https://github.com/prelude-so/python-sdk/commit/72f7709ffeae6b6b4ad835f0861136d713aa3ba5)) +* **internal:** fix lint error on Python 3.14 ([6fa1e9a](https://github.com/prelude-so/python-sdk/commit/6fa1e9a65dd600399a33c4184b159693dc5f6fd2)) +* **internal:** make `test_proxy_environment_variables` more resilient ([1ca0a6e](https://github.com/prelude-so/python-sdk/commit/1ca0a6ec1b01fc52bf7c02225a4a63873da0a75b)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([0e5b065](https://github.com/prelude-so/python-sdk/commit/0e5b065a92f740de2a59ad94777ca4890b0aba13)) +* **internal:** update `actions/checkout` version ([34bf2ba](https://github.com/prelude-so/python-sdk/commit/34bf2ba6a9e133d5287719459da01be9289b115d)) +* speedup initial import ([5300f18](https://github.com/prelude-so/python-sdk/commit/5300f18a10c0db7b222c1bb3e487402aab9d318e)) +* **test:** do not count install time for mock server timeout ([73aaccf](https://github.com/prelude-so/python-sdk/commit/73aaccf6b2dde8d80b4ee5d090a9057f847b4bac)) +* update mock server docs ([85a5da0](https://github.com/prelude-so/python-sdk/commit/85a5da01c2f04110317bd8a8c5a4cec4bd7be831)) + ## 0.10.0 (2025-12-05) Full Changelog: [v0.9.0...v0.10.0](https://github.com/prelude-so/python-sdk/compare/v0.9.0...v0.10.0) diff --git a/pyproject.toml b/pyproject.toml index 01cdbbb..e0a3060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "prelude-python-sdk" -version = "0.10.0" +version = "0.11.0" description = "The official Python library for the Prelude API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/prelude_python_sdk/_version.py b/src/prelude_python_sdk/_version.py index 6d65db6..3f45685 100644 --- a/src/prelude_python_sdk/_version.py +++ b/src/prelude_python_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "prelude_python_sdk" -__version__ = "0.10.0" # x-release-please-version +__version__ = "0.11.0" # x-release-please-version