From 4108c611f50e495b89b21fa8edc8ccdff04602b5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:09:47 +0100 Subject: [PATCH] release: 0.2.0 (#57) * chore(internal): change default timeout to an int (#56) * chore(internal): bummp ruff dependency (#58) * feat(client): send `X-Stainless-Read-Timeout` header (#59) * chore(internal): fix type traversing dictionary params (#60) * chore(internal): minor type handling changes (#61) * chore(internal): update client tests (#62) * chore(internal): codegen related update (#63) * feat(client): allow passing `NotGiven` for body (#64) fix(client): mark some request bodies as optional * chore(internal): fix devcontainers setup (#65) * chore(internal): properly set __pydantic_private__ (#66) * chore(internal): codegen related update (#67) * chore(internal): codegen related update (#68) * docs: update URLs from stainlessapi.com to stainless.com (#69) More details at https://www.stainless.com/changelog/stainless-com * chore(docs): update client docstring (#70) * chore(internal): codegen related update (#71) * docs: revise readme docs about nested params (#72) * test: add DEFER_PYDANTIC_BUILD=false flag to tests (#73) * feat(api): update via SDK Studio (#74) * release: 0.2.0 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 + .release-please-manifest.json | 2 +- .stats.yml | 2 +- CHANGELOG.md | 37 ++++++ README.md | 20 ++- SECURITY.md | 4 +- pyproject.toml | 4 +- requirements-dev.lock | 2 +- scripts/test | 2 + scripts/utils/ruffen-docs.py | 4 +- src/prelude_python_sdk/_base_client.py | 118 +++--------------- src/prelude_python_sdk/_client.py | 2 +- src/prelude_python_sdk/_constants.py | 2 +- src/prelude_python_sdk/_models.py | 10 +- src/prelude_python_sdk/_utils/_sync.py | 19 ++- src/prelude_python_sdk/_utils/_transform.py | 12 +- src/prelude_python_sdk/_version.py | 2 +- .../resources/verification.py | 18 ++- src/prelude_python_sdk/resources/watch.py | 12 +- .../types/verification_check_params.py | 12 +- .../types/verification_create_params.py | 42 +++++-- .../types/watch_feed_back_params.py | 12 +- .../types/watch_predict_params.py | 12 +- tests/api_resources/test_verification.py | 10 +- tests/test_client.py | 54 +++++--- tests/test_transform.py | 11 +- 27 files changed, 258 insertions(+), 172 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac9a2e7..55d2025 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,4 +6,4 @@ USER vscode RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH -RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bbeb30b..c17fdc1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,6 +24,9 @@ } } } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} } // Features to add to the dev container. More info: https://containers.dev/features. diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d2ac0b..10f3091 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "0.2.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index f7be301..6028d62 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ba7ca51ae8c674c16740d0254504bc208d0c3ea5f9970e347e49e70a22dd9072.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ca3f4103971d8bfdb9ea7c345b6112409a62e183460acd29da40a155192d2213.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e11fe..1961b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 0.2.0 (2025-03-11) + +Full Changelog: [v0.1.0...v0.2.0](https://github.com/prelude-so/python-sdk/compare/v0.1.0...v0.2.0) + +### Features + +* **api:** update via SDK Studio ([#74](https://github.com/prelude-so/python-sdk/issues/74)) ([f9658f1](https://github.com/prelude-so/python-sdk/commit/f9658f1ebacf25f72ae9a8e9076958055c2de570)) +* **client:** allow passing `NotGiven` for body ([#64](https://github.com/prelude-so/python-sdk/issues/64)) ([b32f989](https://github.com/prelude-so/python-sdk/commit/b32f98934973c8c2cfacd3ad9a6c0817405ec3c9)) +* **client:** send `X-Stainless-Read-Timeout` header ([#59](https://github.com/prelude-so/python-sdk/issues/59)) ([6dcc82a](https://github.com/prelude-so/python-sdk/commit/6dcc82a592bdad9316eae8ab7b93095d2176caf3)) + + +### Bug Fixes + +* **client:** mark some request bodies as optional ([b32f989](https://github.com/prelude-so/python-sdk/commit/b32f98934973c8c2cfacd3ad9a6c0817405ec3c9)) + + +### Chores + +* **docs:** update client docstring ([#70](https://github.com/prelude-so/python-sdk/issues/70)) ([61cec66](https://github.com/prelude-so/python-sdk/commit/61cec666606b1999db0d6c7bc08e77aa2aed869e)) +* **internal:** bummp ruff dependency ([#58](https://github.com/prelude-so/python-sdk/issues/58)) ([2381d4a](https://github.com/prelude-so/python-sdk/commit/2381d4a22cfd032f97470e0d012b6fc8a133305c)) +* **internal:** change default timeout to an int ([#56](https://github.com/prelude-so/python-sdk/issues/56)) ([160f11e](https://github.com/prelude-so/python-sdk/commit/160f11e767ab3d5f7f1fdd8e423a6277a651fc82)) +* **internal:** codegen related update ([#63](https://github.com/prelude-so/python-sdk/issues/63)) ([0516484](https://github.com/prelude-so/python-sdk/commit/05164849027af87dba0911340086b7904a7171f2)) +* **internal:** codegen related update ([#67](https://github.com/prelude-so/python-sdk/issues/67)) ([32798a9](https://github.com/prelude-so/python-sdk/commit/32798a95e57769f4fc29abac8ba2dcd58d55a6ef)) +* **internal:** codegen related update ([#68](https://github.com/prelude-so/python-sdk/issues/68)) ([f921517](https://github.com/prelude-so/python-sdk/commit/f921517c1c0b0c8197886f6948cecf56a1fdea87)) +* **internal:** codegen related update ([#71](https://github.com/prelude-so/python-sdk/issues/71)) ([ec7fd9f](https://github.com/prelude-so/python-sdk/commit/ec7fd9feb6f45caf98d9667072910b6a0ebfc25d)) +* **internal:** fix devcontainers setup ([#65](https://github.com/prelude-so/python-sdk/issues/65)) ([da3f6c6](https://github.com/prelude-so/python-sdk/commit/da3f6c6f48241dfe0909aabeb3eec2ba83c0e8ef)) +* **internal:** fix type traversing dictionary params ([#60](https://github.com/prelude-so/python-sdk/issues/60)) ([9bf6b95](https://github.com/prelude-so/python-sdk/commit/9bf6b958c8b1ac01d191fb3ffdad7beb9ad0f06a)) +* **internal:** minor type handling changes ([#61](https://github.com/prelude-so/python-sdk/issues/61)) ([0639a28](https://github.com/prelude-so/python-sdk/commit/0639a28c925209b6d2adb2d3022f350044bf5995)) +* **internal:** properly set __pydantic_private__ ([#66](https://github.com/prelude-so/python-sdk/issues/66)) ([affe056](https://github.com/prelude-so/python-sdk/commit/affe056afdc01fc46d7dc23a003b69bb8528c16d)) +* **internal:** update client tests ([#62](https://github.com/prelude-so/python-sdk/issues/62)) ([6096c2a](https://github.com/prelude-so/python-sdk/commit/6096c2aff213dca771b4e8f8675569e1bc1d1edf)) + + +### Documentation + +* revise readme docs about nested params ([#72](https://github.com/prelude-so/python-sdk/issues/72)) ([bff24a7](https://github.com/prelude-so/python-sdk/commit/bff24a785fbd56e126249cbfbc8f2af5c179b8a6)) +* update URLs from stainlessapi.com to stainless.com ([#69](https://github.com/prelude-so/python-sdk/issues/69)) ([f3c2dc7](https://github.com/prelude-so/python-sdk/commit/f3c2dc7a219c04490aa22cdab677410136dc09d3)) + ## 0.1.0 (2025-02-05) Full Changelog: [v0.1.0-beta.1...v0.1.0](https://github.com/prelude-so/python-sdk/compare/v0.1.0-beta.1...v0.1.0) diff --git a/README.md b/README.md index 3fba965..36d42ad 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Prelude Python library provides convenient access to the Prelude REST API fr application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). -It is generated with [Stainless](https://www.stainlessapi.com/). +It is generated with [Stainless](https://www.stainless.com/). ## Documentation @@ -83,6 +83,24 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from prelude_python_sdk import Prelude + +client = Prelude() + +verification = client.verification.create( + target={ + "type": "phone_number", + "value": "+30123456789", + }, +) +print(verification.target) +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `prelude_python_sdk.APIConnectionError` is raised. diff --git a/SECURITY.md b/SECURITY.md index b80688e..deb37e3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Reporting Security Issues -This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. -To report a security issue, please contact the Stainless team at security@stainlessapi.com. +To report a security issue, please contact the Stainless team at security@stainless.com. ## Responsible Disclosure diff --git a/pyproject.toml b/pyproject.toml index 8169a86..619f4b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "prelude-python-sdk" -version = "0.1.0" +version = "0.2.0" description = "The official Python library for the Prelude API" dynamic = ["readme"] license = "Apache-2.0" @@ -177,7 +177,7 @@ select = [ "T201", "T203", # misuse of typing.TYPE_CHECKING - "TCH004", + "TC004", # import rules "TID251", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 5981b85..baa8124 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -78,7 +78,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.22.0 rich==13.7.1 -ruff==0.6.9 +ruff==0.9.4 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/scripts/test b/scripts/test index 4fa5698..2b87845 100755 --- a/scripts/test +++ b/scripts/test @@ -52,6 +52,8 @@ else echo fi +export DEFER_PYDANTIC_BUILD=false + echo "==> Running tests" rye run pytest "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py index 37b3d94..0cf2bd2 100644 --- a/scripts/utils/ruffen-docs.py +++ b/scripts/utils/ruffen-docs.py @@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str: with _collect_error(match): code = format_code_block(code) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" def _pycon_match(match: Match[str]) -> str: code = "" @@ -97,7 +97,7 @@ def finish_fragment() -> None: def _md_pycon_match(match: Match[str]) -> str: code = _pycon_match(match) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) diff --git a/src/prelude_python_sdk/_base_client.py b/src/prelude_python_sdk/_base_client.py index e2d89d2..b034c0c 100644 --- a/src/prelude_python_sdk/_base_client.py +++ b/src/prelude_python_sdk/_base_client.py @@ -9,7 +9,6 @@ import inspect import logging import platform -import warnings import email.utils from types import TracebackType from random import random @@ -36,7 +35,7 @@ import httpx import distro import pydantic -from httpx import URL, Limits +from httpx import URL from pydantic import PrivateAttr from . import _exceptions @@ -51,19 +50,16 @@ Timeout, NotGiven, ResponseT, - Transport, AnyMapping, PostParser, - ProxiesTypes, RequestFiles, HttpxSendArgs, - AsyncTransport, RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import model_copy, model_dump +from ._compat import PYDANTIC_V2, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -207,6 +203,9 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -292,6 +291,9 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -331,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): _base_url: URL max_retries: int timeout: Union[float, Timeout, None] - _limits: httpx.Limits - _proxies: ProxiesTypes | None - _transport: Transport | AsyncTransport | None _strict_response_validation: bool _idempotency_header: str | None _default_stream_cls: type[_DefaultStreamT] | None = None @@ -346,9 +345,6 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None = DEFAULT_TIMEOUT, - limits: httpx.Limits, - transport: Transport | AsyncTransport | None, - proxies: ProxiesTypes | None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: @@ -356,9 +352,6 @@ def __init__( self._base_url = self._enforce_trailing_slash(URL(base_url)) self.max_retries = max_retries self.timeout = timeout - self._limits = limits - self._proxies = proxies - self._transport = transport self._custom_headers = custom_headers or {} self._custom_query = custom_query or {} self._strict_response_validation = _strict_response_validation @@ -418,10 +411,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - # Don't set the retry count header if it was already set or removed by the caller. We check + # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) return headers @@ -511,7 +511,7 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data, + json=json_data if is_given(json_data) else None, files=files, **kwargs, ) @@ -787,46 +787,11 @@ def __init__( base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: Transport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -847,12 +812,9 @@ def __init__( super().__init__( version=version, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, base_url=base_url, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -862,9 +824,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1359,45 +1318,10 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: AsyncTransport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -1419,11 +1343,8 @@ def __init__( super().__init__( version=version, base_url=base_url, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -1433,9 +1354,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: diff --git a/src/prelude_python_sdk/_client.py b/src/prelude_python_sdk/_client.py index e7706c8..e2ad734 100644 --- a/src/prelude_python_sdk/_client.py +++ b/src/prelude_python_sdk/_client.py @@ -241,7 +241,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async Prelude client instance. + """Construct a new async AsyncPrelude client instance. This automatically infers the `api_token` argument from the `API_TOKEN` environment variable if it is not provided. """ diff --git a/src/prelude_python_sdk/_constants.py b/src/prelude_python_sdk/_constants.py index a2ac3b6..6ddf2c7 100644 --- a/src/prelude_python_sdk/_constants.py +++ b/src/prelude_python_sdk/_constants.py @@ -6,7 +6,7 @@ OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" # default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) diff --git a/src/prelude_python_sdk/_models.py b/src/prelude_python_sdk/_models.py index 9a918aa..c4401ff 100644 --- a/src/prelude_python_sdk/_models.py +++ b/src/prelude_python_sdk/_models.py @@ -172,7 +172,7 @@ def to_json( @override def __str__(self) -> str: # mypy complains about an invalid self arg - return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] # Override the 'construct' method in a way that supports recursive parsing without validation. # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object: If the given value does not match the expected type then it is returned as-is. """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` @@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object: if is_union(origin): try: - return validate_type(type_=cast("type[object]", type_), value=value) + return validate_type(type_=cast("type[object]", original_type or type_), value=value) except Exception: pass diff --git a/src/prelude_python_sdk/_utils/_sync.py b/src/prelude_python_sdk/_utils/_sync.py index 8b3aaf2..ad7ec71 100644 --- a/src/prelude_python_sdk/_utils/_sync.py +++ b/src/prelude_python_sdk/_utils/_sync.py @@ -7,16 +7,20 @@ from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec +import anyio +import sniffio +import anyio.to_thread + T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") if sys.version_info >= (3, 9): - to_thread = asyncio.to_thread + _asyncio_to_thread = asyncio.to_thread else: # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread # for Python 3.8 support - async def to_thread( + async def _asyncio_to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> Any: """Asynchronously run function *func* in a separate thread. @@ -34,6 +38,17 @@ async def to_thread( return await loop.run_in_executor(None, func_call) +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await _asyncio_to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + # inspired by `asyncer`, https://github.com/tiangolo/asyncer def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ diff --git a/src/prelude_python_sdk/_utils/_transform.py b/src/prelude_python_sdk/_utils/_transform.py index a6b62ca..18afd9d 100644 --- a/src/prelude_python_sdk/_utils/_transform.py +++ b/src/prelude_python_sdk/_utils/_transform.py @@ -25,7 +25,7 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import model_dump, is_typeddict +from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -164,9 +164,14 @@ def _transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return _transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) @@ -307,9 +312,14 @@ async def _async_transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return await _async_transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) diff --git a/src/prelude_python_sdk/_version.py b/src/prelude_python_sdk/_version.py index 6e6c7ca..9122928 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.1.0" # x-release-please-version +__version__ = "0.2.0" # x-release-please-version diff --git a/src/prelude_python_sdk/resources/verification.py b/src/prelude_python_sdk/resources/verification.py index 735a7cc..7367e88 100644 --- a/src/prelude_python_sdk/resources/verification.py +++ b/src/prelude_python_sdk/resources/verification.py @@ -67,7 +67,8 @@ def create( this endpoint will perform a retry instead. Args: - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. dispatch_id: The identifier of the dispatch that came from the front-end SDK. @@ -76,7 +77,8 @@ def create( options: Verification options - signals: The signals used for anti-fraud. + signals: The signals used for anti-fraud. For more details, refer to + [Signals](/guides/prevent-fraud#signals). extra_headers: Send extra headers @@ -122,7 +124,8 @@ def check( Args: code: The OTP code to validate. - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. extra_headers: Send extra headers @@ -190,7 +193,8 @@ async def create( this endpoint will perform a retry instead. Args: - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. dispatch_id: The identifier of the dispatch that came from the front-end SDK. @@ -199,7 +203,8 @@ async def create( options: Verification options - signals: The signals used for anti-fraud. + signals: The signals used for anti-fraud. For more details, refer to + [Signals](/guides/prevent-fraud#signals). extra_headers: Send extra headers @@ -245,7 +250,8 @@ async def check( Args: code: The OTP code to validate. - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. extra_headers: Send extra headers diff --git a/src/prelude_python_sdk/resources/watch.py b/src/prelude_python_sdk/resources/watch.py index 0233667..0907e8b 100644 --- a/src/prelude_python_sdk/resources/watch.py +++ b/src/prelude_python_sdk/resources/watch.py @@ -65,7 +65,8 @@ def feed_back( feedback: You should send a feedback event back to Watch API when your user demonstrates authentic behavior. - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. extra_headers: Send extra headers @@ -108,7 +109,8 @@ def predict( must be implemented in conjunction with the `watch/feedback` endpoint. Args: - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. signals: It is highly recommended that you provide the signals to increase prediction performance. @@ -177,7 +179,8 @@ async def feed_back( feedback: You should send a feedback event back to Watch API when your user demonstrates authentic behavior. - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. extra_headers: Send extra headers @@ -220,7 +223,8 @@ async def predict( must be implemented in conjunction with the `watch/feedback` endpoint. Args: - target: The target. Currently this can only be an E.164 formatted phone number. + target: The verification target. Either a phone number or an email address. To use the + email verification feature contact us to discuss your use case. signals: It is highly recommended that you provide the signals to increase prediction performance. diff --git a/src/prelude_python_sdk/types/verification_check_params.py b/src/prelude_python_sdk/types/verification_check_params.py index ad7b8eb..dea9702 100644 --- a/src/prelude_python_sdk/types/verification_check_params.py +++ b/src/prelude_python_sdk/types/verification_check_params.py @@ -12,12 +12,16 @@ class VerificationCheckParams(TypedDict, total=False): """The OTP code to validate.""" target: Required[Target] - """The target. Currently this can only be an E.164 formatted phone number.""" + """The verification target. + + Either a phone number or an email address. To use the email verification feature + contact us to discuss your use case. + """ class Target(TypedDict, total=False): - type: Required[Literal["phone_number"]] - """The type of the target. Currently this can only be "phone_number".""" + type: Required[Literal["phone_number", "email_address"]] + """The type of the target. Either "phone_number" or "email_address".""" value: Required[str] - """An E.164 formatted phone number to verify.""" + """An E.164 formatted phone number or an email address.""" diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index 9ea69a7..15a83b6 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Literal, Required, TypedDict __all__ = ["VerificationCreateParams", "Target", "Metadata", "Options", "OptionsAppRealm", "Signals"] @@ -9,7 +10,11 @@ class VerificationCreateParams(TypedDict, total=False): target: Required[Target] - """The target. Currently this can only be an E.164 formatted phone number.""" + """The verification target. + + Either a phone number or an email address. To use the email verification feature + contact us to discuss your use case. + """ dispatch_id: str """The identifier of the dispatch that came from the front-end SDK.""" @@ -25,15 +30,18 @@ class VerificationCreateParams(TypedDict, total=False): """Verification options""" signals: Signals - """The signals used for anti-fraud.""" + """The signals used for anti-fraud. + + For more details, refer to [Signals](/guides/prevent-fraud#signals). + """ class Target(TypedDict, total=False): - type: Required[Literal["phone_number"]] - """The type of the target. Currently this can only be "phone_number".""" + type: Required[Literal["phone_number", "email_address"]] + """The type of the target. Either "phone_number" or "email_address".""" value: Required[str] - """An E.164 formatted phone number to verify.""" + """An E.164 formatted phone number or an email address.""" class Metadata(TypedDict, total=False): @@ -59,6 +67,13 @@ class Options(TypedDict, total=False): Currently only Android devices are supported. """ + callback_url: str + """ + The URL where webhooks will be sent when verification events occur, including + verification creation, attempt creation, and delivery status changes. For more + details, refer to [Webhook](/api-reference/v2/verify/webhook). + """ + code_size: int """The size of the code generated. @@ -89,12 +104,15 @@ class Options(TypedDict, total=False): """ template_id: str - """The identifier of a verification settings template. + """The identifier of a verification template. - It is used to be able to switch behavior for specific use cases. Contact us if - you need to use this functionality. + It applies use case-specific settings, such as the message content or certain + verification parameters. """ + variables: Dict[str, str] + """The variables to be replaced in the template.""" + class Signals(TypedDict, total=False): app_version: str @@ -124,3 +142,11 @@ class Signals(TypedDict, total=False): 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. + """ diff --git a/src/prelude_python_sdk/types/watch_feed_back_params.py b/src/prelude_python_sdk/types/watch_feed_back_params.py index a7853fa..c4cc330 100644 --- a/src/prelude_python_sdk/types/watch_feed_back_params.py +++ b/src/prelude_python_sdk/types/watch_feed_back_params.py @@ -15,7 +15,11 @@ class WatchFeedBackParams(TypedDict, total=False): """ target: Required[Target] - """The target. Currently this can only be an E.164 formatted phone number.""" + """The verification target. + + Either a phone number or an email address. To use the email verification feature + contact us to discuss your use case. + """ class Feedback(TypedDict, total=False): @@ -27,8 +31,8 @@ class Feedback(TypedDict, total=False): class Target(TypedDict, total=False): - type: Required[Literal["phone_number"]] - """The type of the target. Currently this can only be "phone_number".""" + type: Required[Literal["phone_number", "email_address"]] + """The type of the target. Either "phone_number" or "email_address".""" value: Required[str] - """An E.164 formatted phone number to verify.""" + """An E.164 formatted phone number or an email address.""" diff --git a/src/prelude_python_sdk/types/watch_predict_params.py b/src/prelude_python_sdk/types/watch_predict_params.py index 7d8a645..6ef2093 100644 --- a/src/prelude_python_sdk/types/watch_predict_params.py +++ b/src/prelude_python_sdk/types/watch_predict_params.py @@ -9,7 +9,11 @@ class WatchPredictParams(TypedDict, total=False): target: Required[Target] - """The target. Currently this can only be an E.164 formatted phone number.""" + """The verification target. + + Either a phone number or an email address. To use the email verification feature + contact us to discuss your use case. + """ signals: Signals """ @@ -19,11 +23,11 @@ class WatchPredictParams(TypedDict, total=False): class Target(TypedDict, total=False): - type: Required[Literal["phone_number"]] - """The type of the target. Currently this can only be "phone_number".""" + type: Required[Literal["phone_number", "email_address"]] + """The type of the target. Either "phone_number" or "email_address".""" value: Required[str] - """An E.164 formatted phone number to verify.""" + """An E.164 formatted phone number or an email address.""" class Signals(TypedDict, total=False): diff --git a/tests/api_resources/test_verification.py b/tests/api_resources/test_verification.py index 6170e67..d88eca4 100644 --- a/tests/api_resources/test_verification.py +++ b/tests/api_resources/test_verification.py @@ -44,11 +44,13 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: "platform": "android", "value": "value", }, + "callback_url": "callback_url", "code_size": 5, "custom_code": "custom_code", "locale": "el-GR", "sender_id": "sender_id", - "template_id": "template_id", + "template_id": "prelude:psd2", + "variables": {"foo": "bar"}, }, signals={ "app_version": "1.2.34", @@ -58,6 +60,7 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: "ip": "192.0.2.1", "is_trusted_user": False, "os_version": "18.0.1", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1", }, ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) @@ -163,11 +166,13 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - "platform": "android", "value": "value", }, + "callback_url": "callback_url", "code_size": 5, "custom_code": "custom_code", "locale": "el-GR", "sender_id": "sender_id", - "template_id": "template_id", + "template_id": "prelude:psd2", + "variables": {"foo": "bar"}, }, signals={ "app_version": "1.2.34", @@ -177,6 +182,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - "ip": "192.0.2.1", "is_trusted_user": False, "os_version": "18.0.1", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1", }, ) assert_matches_type(VerificationCreateResponse, verification, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index 71af557..a9d9e60 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,6 +23,7 @@ from prelude_python_sdk import Prelude, AsyncPrelude, APIResponseValidationError from prelude_python_sdk._types import Omit +from prelude_python_sdk._utils import maybe_transform from prelude_python_sdk._models import BaseModel, FinalRequestOptions from prelude_python_sdk._constants import RAW_RESPONSE_HEADER from prelude_python_sdk._exceptions import PreludeError, APIStatusError, APITimeoutError, APIResponseValidationError @@ -32,6 +33,7 @@ BaseClient, make_request_options, ) +from prelude_python_sdk.types.verification_create_params import VerificationCreateParams from .utils import update_env @@ -732,11 +734,14 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "/v2/verification", body=cast( object, - dict( - target={ - "type": "phone_number", - "value": "+30123456789", - } + maybe_transform( + dict( + target={ + "type": "phone_number", + "value": "+30123456789", + } + ), + VerificationCreateParams, ), ), cast_to=httpx.Response, @@ -755,11 +760,14 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "/v2/verification", body=cast( object, - dict( - target={ - "type": "phone_number", - "value": "+30123456789", - } + maybe_transform( + dict( + target={ + "type": "phone_number", + "value": "+30123456789", + } + ), + VerificationCreateParams, ), ), cast_to=httpx.Response, @@ -1544,11 +1552,14 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "/v2/verification", body=cast( object, - dict( - target={ - "type": "phone_number", - "value": "+30123456789", - } + maybe_transform( + dict( + target={ + "type": "phone_number", + "value": "+30123456789", + } + ), + VerificationCreateParams, ), ), cast_to=httpx.Response, @@ -1567,11 +1578,14 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "/v2/verification", body=cast( object, - dict( - target={ - "type": "phone_number", - "value": "+30123456789", - } + maybe_transform( + dict( + target={ + "type": "phone_number", + "value": "+30123456789", + } + ), + VerificationCreateParams, ), ), cast_to=httpx.Response, diff --git a/tests/test_transform.py b/tests/test_transform.py index ed8a80c..b48736d 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -2,7 +2,7 @@ import io import pathlib -from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast from datetime import date, datetime from typing_extensions import Required, Annotated, TypedDict @@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]: } +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + class TypedDictIterableUnionStr(TypedDict): foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]