From b57dc83678fb8f51d3dbb847b18c6edab89ce199 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:01:46 +0200 Subject: [PATCH] release: 0.3.0 (#77) * chore(internal): remove extra empty newlines (#76) * chore(internal): bump rye to 0.44.0 (#78) * chore(internal): codegen related update (#79) * fix(types): handle more discriminated union shapes (#80) * fix(ci): ensure pip is always available (#81) * fix(ci): remove publishing patch (#82) * codegen metadata * chore: fix typos (#83) * codegen metadata * codegen metadata * chore(internal): remove trailing character (#84) * chore(internal): slight transform perf improvement (#85) * chore(tests): improve enum examples (#86) * chore(internal): expand CI branch coverage (#87) * chore(internal): reduce CI branch coverage * fix(perf): skip traversing types for NotGiven values * fix(perf): optimize some hot paths * feat(api): update via SDK Studio * feat(api): update via SDK Studio * release: 0.3.0 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/publish-pypi.yml | 2 +- .release-please-manifest.json | 2 +- .stats.yml | 6 +- CHANGELOG.md | 31 ++ api.md | 21 +- bin/publish-pypi | 3 - pyproject.toml | 6 +- requirements-dev.lock | 1 + requirements.lock | 1 + src/prelude_python_sdk/_client.py | 10 +- src/prelude_python_sdk/_models.py | 9 +- src/prelude_python_sdk/_utils/_transform.py | 49 ++- src/prelude_python_sdk/_utils/_typing.py | 2 + src/prelude_python_sdk/_version.py | 2 +- src/prelude_python_sdk/resources/__init__.py | 14 + src/prelude_python_sdk/resources/lookup.py | 199 +++++++++++ .../resources/verification.py | 22 +- src/prelude_python_sdk/resources/watch.py | 237 ++++++++----- src/prelude_python_sdk/types/__init__.py | 8 +- .../types/lookup_lookup_params.py | 17 + .../types/lookup_lookup_response.py | 116 +++++++ .../types/verification_create_params.py | 19 +- .../types/watch_feed_back_params.py | 38 --- .../types/watch_feed_back_response.py | 11 - .../types/watch_predict_params.py | 49 ++- .../types/watch_predict_response.py | 26 +- .../types/watch_send_events_params.py | 32 ++ .../types/watch_send_events_response.py | 18 + .../types/watch_send_feedbacks_params.py | 86 +++++ .../types/watch_send_feedbacks_response.py | 18 + tests/api_resources/test_lookup.py | 114 +++++++ tests/api_resources/test_verification.py | 6 +- tests/api_resources/test_watch.py | 313 +++++++++++++----- tests/test_client.py | 2 +- tests/test_models.py | 32 ++ tests/test_transform.py | 21 +- 38 files changed, 1278 insertions(+), 273 deletions(-) create mode 100644 src/prelude_python_sdk/resources/lookup.py create mode 100644 src/prelude_python_sdk/types/lookup_lookup_params.py create mode 100644 src/prelude_python_sdk/types/lookup_lookup_response.py delete mode 100644 src/prelude_python_sdk/types/watch_feed_back_params.py delete mode 100644 src/prelude_python_sdk/types/watch_feed_back_response.py create mode 100644 src/prelude_python_sdk/types/watch_send_events_params.py create mode 100644 src/prelude_python_sdk/types/watch_send_events_response.py create mode 100644 src/prelude_python_sdk/types/watch_send_feedbacks_params.py create mode 100644 src/prelude_python_sdk/types/watch_send_feedbacks_response.py create mode 100644 tests/api_resources/test_lookup.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 55d2025..ff261ba 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8a8a4f..81f6dc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -21,7 +20,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Install dependencies @@ -33,7 +32,6 @@ jobs: test: name: test runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -42,7 +40,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b1deba8..1ccb8d9 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,7 +21,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Publish to PyPI diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f3091..6b7b74c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.3.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 6028d62..2bb2a0e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,4 @@ -configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-ca3f4103971d8bfdb9ea7c345b6112409a62e183460acd29da40a155192d2213.yml +configured_endpoints: 7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/prelude%2Fprelude-d14cd19a9858e9e255cc1e21227462f83b2964beac2022e77203e4d60020ff74.yml +openapi_spec_hash: ecbc1fb11f938b50db2b7d0a9aa325fa +config_hash: 3b3da533cbc4a28dd64565b6c3fa3dd0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1961b2f..418704f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 0.3.0 (2025-04-11) + +Full Changelog: [v0.2.0...v0.3.0](https://github.com/prelude-so/python-sdk/compare/v0.2.0...v0.3.0) + +### Features + +* **api:** update via SDK Studio ([e8db40d](https://github.com/prelude-so/python-sdk/commit/e8db40d0c6bb7ed120d01c7a5133e84611fa2dc5)) +* **api:** update via SDK Studio ([2738f74](https://github.com/prelude-so/python-sdk/commit/2738f749089da145689c78aabdedf810d3329826)) + + +### Bug Fixes + +* **ci:** ensure pip is always available ([#81](https://github.com/prelude-so/python-sdk/issues/81)) ([3496a08](https://github.com/prelude-so/python-sdk/commit/3496a088c4a51ff9755df7d5537031d2b66224b8)) +* **ci:** remove publishing patch ([#82](https://github.com/prelude-so/python-sdk/issues/82)) ([00fa879](https://github.com/prelude-so/python-sdk/commit/00fa8799dc14bc3d2dae941485f2e3a24bfb2bf3)) +* **perf:** optimize some hot paths ([6203988](https://github.com/prelude-so/python-sdk/commit/6203988ff6273cfe5135ec7d427c620a1094f6a1)) +* **perf:** skip traversing types for NotGiven values ([e5a8fd5](https://github.com/prelude-so/python-sdk/commit/e5a8fd59dd7168e68ff026e9d11d796e3d002241)) +* **types:** handle more discriminated union shapes ([#80](https://github.com/prelude-so/python-sdk/issues/80)) ([716195b](https://github.com/prelude-so/python-sdk/commit/716195b1874b4ec76cd39465810e3500c756eae8)) + + +### Chores + +* fix typos ([#83](https://github.com/prelude-so/python-sdk/issues/83)) ([ab98ad3](https://github.com/prelude-so/python-sdk/commit/ab98ad32961298cf1a2f47e6b3cc66a9f69cddbc)) +* **internal:** bump rye to 0.44.0 ([#78](https://github.com/prelude-so/python-sdk/issues/78)) ([436ceca](https://github.com/prelude-so/python-sdk/commit/436ceca01c22fd4015010d5bd3852ce319d0ed65)) +* **internal:** codegen related update ([#79](https://github.com/prelude-so/python-sdk/issues/79)) ([e5e9c6d](https://github.com/prelude-so/python-sdk/commit/e5e9c6d643232cad96a285dd7dc662f98684fbdc)) +* **internal:** expand CI branch coverage ([#87](https://github.com/prelude-so/python-sdk/issues/87)) ([3edb1aa](https://github.com/prelude-so/python-sdk/commit/3edb1aab16d2b38705d64969e9ac70fe00951ee6)) +* **internal:** reduce CI branch coverage ([70118ea](https://github.com/prelude-so/python-sdk/commit/70118ea5611c2f4337b21ac7da52a740bc52d7ff)) +* **internal:** remove extra empty newlines ([#76](https://github.com/prelude-so/python-sdk/issues/76)) ([3e52319](https://github.com/prelude-so/python-sdk/commit/3e5231901ad7bcc6e06a4c82aeaa619f759434f7)) +* **internal:** remove trailing character ([#84](https://github.com/prelude-so/python-sdk/issues/84)) ([526b990](https://github.com/prelude-so/python-sdk/commit/526b990f47cf42a44064d85ed2d8f9acbe35a609)) +* **internal:** slight transform perf improvement ([#85](https://github.com/prelude-so/python-sdk/issues/85)) ([b77e93b](https://github.com/prelude-so/python-sdk/commit/b77e93ba797273dd8a145183d9b9c712659163cd)) +* **tests:** improve enum examples ([#86](https://github.com/prelude-so/python-sdk/issues/86)) ([140d696](https://github.com/prelude-so/python-sdk/commit/140d6966a00666b4ade42fef7de7ec701cf697d3)) + ## 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) diff --git a/api.md b/api.md index 041f85b..87b5a5c 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,15 @@ +# Lookup + +Types: + +```python +from prelude_python_sdk.types import LookupLookupResponse +``` + +Methods: + +- client.lookup.lookup(phone_number, \*\*params) -> LookupLookupResponse + # Transactional Types: @@ -28,10 +40,15 @@ Methods: Types: ```python -from prelude_python_sdk.types import WatchFeedBackResponse, WatchPredictResponse +from prelude_python_sdk.types import ( + WatchPredictResponse, + WatchSendEventsResponse, + WatchSendFeedbacksResponse, +) ``` Methods: -- client.watch.feed_back(\*\*params) -> WatchFeedBackResponse - client.watch.predict(\*\*params) -> WatchPredictResponse +- client.watch.send_events(\*\*params) -> WatchSendEventsResponse +- client.watch.send_feedbacks(\*\*params) -> WatchSendFeedbacksResponse diff --git a/bin/publish-pypi b/bin/publish-pypi index 05bfccb..826054e 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,7 +3,4 @@ set -eux mkdir -p dist rye build --clean -# Patching importlib-metadata version until upstream library version is updated -# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 -"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index 619f4b3..b609f29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "prelude-python-sdk" -version = "0.2.0" +version = "0.3.0" description = "The official Python library for the Prelude API" dynamic = ["readme"] license = "Apache-2.0" @@ -38,7 +38,6 @@ Homepage = "https://github.com/prelude-so/python-sdk" Repository = "https://github.com/prelude-so/python-sdk" - [tool.rye] managed = true # version pins are in requirements-dev.lock @@ -87,7 +86,7 @@ typecheck = { chain = [ "typecheck:mypy" = "mypy ." [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [tool.hatch.build] @@ -152,7 +151,6 @@ reportImplicitOverride = true reportImportCycles = false reportPrivateUsage = false - [tool.ruff] line-length = 120 output-format = "grouped" diff --git a/requirements-dev.lock b/requirements-dev.lock index baa8124..a35ea0c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 diff --git a/requirements.lock b/requirements.lock index 2fe1f85..c5e53b6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 diff --git a/src/prelude_python_sdk/_client.py b/src/prelude_python_sdk/_client.py index e2ad734..fae0d87 100644 --- a/src/prelude_python_sdk/_client.py +++ b/src/prelude_python_sdk/_client.py @@ -24,7 +24,7 @@ get_async_library, ) from ._version import __version__ -from .resources import watch, verification, transactional +from .resources import watch, lookup, verification, transactional from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import PreludeError, APIStatusError from ._base_client import ( @@ -37,6 +37,7 @@ class Prelude(SyncAPIClient): + lookup: lookup.LookupResource transactional: transactional.TransactionalResource verification: verification.VerificationResource watch: watch.WatchResource @@ -97,6 +98,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.lookup = lookup.LookupResource(self) self.transactional = transactional.TransactionalResource(self) self.verification = verification.VerificationResource(self) self.watch = watch.WatchResource(self) @@ -209,6 +211,7 @@ def _make_status_error( class AsyncPrelude(AsyncAPIClient): + lookup: lookup.AsyncLookupResource transactional: transactional.AsyncTransactionalResource verification: verification.AsyncVerificationResource watch: watch.AsyncWatchResource @@ -269,6 +272,7 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self.lookup = lookup.AsyncLookupResource(self) self.transactional = transactional.AsyncTransactionalResource(self) self.verification = verification.AsyncVerificationResource(self) self.watch = watch.AsyncWatchResource(self) @@ -382,6 +386,7 @@ def _make_status_error( class PreludeWithRawResponse: def __init__(self, client: Prelude) -> None: + self.lookup = lookup.LookupResourceWithRawResponse(client.lookup) self.transactional = transactional.TransactionalResourceWithRawResponse(client.transactional) self.verification = verification.VerificationResourceWithRawResponse(client.verification) self.watch = watch.WatchResourceWithRawResponse(client.watch) @@ -389,6 +394,7 @@ def __init__(self, client: Prelude) -> None: class AsyncPreludeWithRawResponse: def __init__(self, client: AsyncPrelude) -> None: + self.lookup = lookup.AsyncLookupResourceWithRawResponse(client.lookup) self.transactional = transactional.AsyncTransactionalResourceWithRawResponse(client.transactional) self.verification = verification.AsyncVerificationResourceWithRawResponse(client.verification) self.watch = watch.AsyncWatchResourceWithRawResponse(client.watch) @@ -396,6 +402,7 @@ def __init__(self, client: AsyncPrelude) -> None: class PreludeWithStreamedResponse: def __init__(self, client: Prelude) -> None: + self.lookup = lookup.LookupResourceWithStreamingResponse(client.lookup) self.transactional = transactional.TransactionalResourceWithStreamingResponse(client.transactional) self.verification = verification.VerificationResourceWithStreamingResponse(client.verification) self.watch = watch.WatchResourceWithStreamingResponse(client.watch) @@ -403,6 +410,7 @@ def __init__(self, client: Prelude) -> None: class AsyncPreludeWithStreamedResponse: def __init__(self, client: AsyncPrelude) -> None: + self.lookup = lookup.AsyncLookupResourceWithStreamingResponse(client.lookup) self.transactional = transactional.AsyncTransactionalResourceWithStreamingResponse(client.transactional) self.verification = verification.AsyncVerificationResourceWithStreamingResponse(client.verification) self.watch = watch.AsyncWatchResourceWithStreamingResponse(client.watch) diff --git a/src/prelude_python_sdk/_models.py b/src/prelude_python_sdk/_models.py index c4401ff..3493571 100644 --- a/src/prelude_python_sdk/_models.py +++ b/src/prelude_python_sdk/_models.py @@ -65,7 +65,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -646,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + if schema["type"] != "model": return None + schema = cast("ModelSchema", schema) fields_schema = schema["schema"] if fields_schema["type"] != "model-fields": return None fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) if not field: return None @@ -678,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: setattr(typ, "__pydantic_config__", config) # noqa: B010 -# our use of subclasssing here causes weirdness for type checkers, +# our use of subclassing here causes weirdness for type checkers, # so we just pretend that we don't subclass if TYPE_CHECKING: GenericModel = BaseModel diff --git a/src/prelude_python_sdk/_utils/_transform.py b/src/prelude_python_sdk/_utils/_transform.py index 18afd9d..b0cc20a 100644 --- a/src/prelude_python_sdk/_utils/_transform.py +++ b/src/prelude_python_sdk/_utils/_transform.py @@ -5,13 +5,15 @@ import pathlib from typing import Any, Mapping, TypeVar, cast from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints import anyio import pydantic from ._utils import ( is_list, + is_given, + lru_cache, is_mapping, is_iterable, ) @@ -108,6 +110,7 @@ class Params(TypedDict, total=False): return cast(_T, transformed) +@lru_cache(maxsize=8096) def _get_annotated_type(type_: type) -> type | None: """If the given type is an `Annotated` type then it is returned, if not `None` is returned. @@ -126,7 +129,7 @@ def _get_annotated_type(type_: type) -> type | None: def _maybe_transform_key(key: str, type_: type) -> str: """Transform the given `data` based on the annotations provided in `type_`. - Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata. + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. """ annotated_type = _get_annotated_type(type_) if annotated_type is None: @@ -142,6 +145,10 @@ def _maybe_transform_key(key: str, type_: type) -> str: return key +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + def _transform_recursive( data: object, *, @@ -184,6 +191,15 @@ def _transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -245,6 +261,11 @@ def _transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -332,6 +353,15 @@ async def _async_transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -393,6 +423,11 @@ async def _async_transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -400,3 +435,13 @@ async def _async_transform_typeddict( else: result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/prelude_python_sdk/_utils/_typing.py b/src/prelude_python_sdk/_utils/_typing.py index 278749b..1958820 100644 --- a/src/prelude_python_sdk/_utils/_typing.py +++ b/src/prelude_python_sdk/_utils/_typing.py @@ -13,6 +13,7 @@ get_origin, ) +from ._utils import lru_cache from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): return strip_annotated_type(cast(type, get_args(typ)[0])) diff --git a/src/prelude_python_sdk/_version.py b/src/prelude_python_sdk/_version.py index 9122928..134a030 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.2.0" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version diff --git a/src/prelude_python_sdk/resources/__init__.py b/src/prelude_python_sdk/resources/__init__.py index 5bf14a5..00ae8f9 100644 --- a/src/prelude_python_sdk/resources/__init__.py +++ b/src/prelude_python_sdk/resources/__init__.py @@ -8,6 +8,14 @@ WatchResourceWithStreamingResponse, AsyncWatchResourceWithStreamingResponse, ) +from .lookup import ( + LookupResource, + AsyncLookupResource, + LookupResourceWithRawResponse, + AsyncLookupResourceWithRawResponse, + LookupResourceWithStreamingResponse, + AsyncLookupResourceWithStreamingResponse, +) from .verification import ( VerificationResource, AsyncVerificationResource, @@ -26,6 +34,12 @@ ) __all__ = [ + "LookupResource", + "AsyncLookupResource", + "LookupResourceWithRawResponse", + "AsyncLookupResourceWithRawResponse", + "LookupResourceWithStreamingResponse", + "AsyncLookupResourceWithStreamingResponse", "TransactionalResource", "AsyncTransactionalResource", "TransactionalResourceWithRawResponse", diff --git a/src/prelude_python_sdk/resources/lookup.py b/src/prelude_python_sdk/resources/lookup.py new file mode 100644 index 0000000..6ce0cc3 --- /dev/null +++ b/src/prelude_python_sdk/resources/lookup.py @@ -0,0 +1,199 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal + +import httpx + +from ..types import lookup_lookup_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.lookup_lookup_response import LookupLookupResponse + +__all__ = ["LookupResource", "AsyncLookupResource"] + + +class LookupResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> LookupResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/prelude-so/python-sdk#accessing-raw-response-data-eg-headers + """ + return LookupResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LookupResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/prelude-so/python-sdk#with_streaming_response + """ + return LookupResourceWithStreamingResponse(self) + + def lookup( + self, + phone_number: str, + *, + type: List[Literal["cnam"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> LookupLookupResponse: + """ + Retrieve detailed information about a phone number including carrier data, line + type, and portability status. + + Args: + phone_number: An E.164 formatted phone number to look up. + + type: + Optional features. Possible values are: + + - `cnam` - Retrieve CNAM (Caller ID Name) along with other information. Contact + us if you need to use this functionality. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not phone_number: + raise ValueError(f"Expected a non-empty value for `phone_number` but received {phone_number!r}") + return self._get( + f"/v2/lookup/{phone_number}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"type": type}, lookup_lookup_params.LookupLookupParams), + ), + cast_to=LookupLookupResponse, + ) + + +class AsyncLookupResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncLookupResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/prelude-so/python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLookupResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLookupResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/prelude-so/python-sdk#with_streaming_response + """ + return AsyncLookupResourceWithStreamingResponse(self) + + async def lookup( + self, + phone_number: str, + *, + type: List[Literal["cnam"]] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> LookupLookupResponse: + """ + Retrieve detailed information about a phone number including carrier data, line + type, and portability status. + + Args: + phone_number: An E.164 formatted phone number to look up. + + type: + Optional features. Possible values are: + + - `cnam` - Retrieve CNAM (Caller ID Name) along with other information. Contact + us if you need to use this functionality. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not phone_number: + raise ValueError(f"Expected a non-empty value for `phone_number` but received {phone_number!r}") + return await self._get( + f"/v2/lookup/{phone_number}", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"type": type}, lookup_lookup_params.LookupLookupParams), + ), + cast_to=LookupLookupResponse, + ) + + +class LookupResourceWithRawResponse: + def __init__(self, lookup: LookupResource) -> None: + self._lookup = lookup + + self.lookup = to_raw_response_wrapper( + lookup.lookup, + ) + + +class AsyncLookupResourceWithRawResponse: + def __init__(self, lookup: AsyncLookupResource) -> None: + self._lookup = lookup + + self.lookup = async_to_raw_response_wrapper( + lookup.lookup, + ) + + +class LookupResourceWithStreamingResponse: + def __init__(self, lookup: LookupResource) -> None: + self._lookup = lookup + + self.lookup = to_streamed_response_wrapper( + lookup.lookup, + ) + + +class AsyncLookupResourceWithStreamingResponse: + def __init__(self, lookup: AsyncLookupResource) -> None: + self._lookup = lookup + + self.lookup = async_to_streamed_response_wrapper( + lookup.lookup, + ) diff --git a/src/prelude_python_sdk/resources/verification.py b/src/prelude_python_sdk/resources/verification.py index 7367e88..550d152 100644 --- a/src/prelude_python_sdk/resources/verification.py +++ b/src/prelude_python_sdk/resources/verification.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing_extensions import Literal + import httpx from ..types import verification_check_params, verification_create_params @@ -51,6 +53,7 @@ def create( target: verification_create_params.Target, dispatch_id: str | NotGiven = NOT_GIVEN, metadata: verification_create_params.Metadata | NotGiven = NOT_GIVEN, + method: Literal["auto", "voice"] | NotGiven = NOT_GIVEN, options: verification_create_params.Options | NotGiven = NOT_GIVEN, signals: verification_create_params.Signals | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -75,10 +78,16 @@ def create( metadata: The metadata for this verification. This object will be returned with every response or webhook sent that refers to this verification. + method: The method used for verifying this phone number. The 'voice' option provides an + accessible alternative for visually impaired users by delivering the + verification code through a phone call rather than a text message. It also + allows verification of landline numbers that cannot receive SMS messages. + **Coming soon.** + options: Verification options signals: The signals used for anti-fraud. For more details, refer to - [Signals](/guides/prevent-fraud#signals). + [Signals](/verify/v2/documentation/prevent-fraud#signals). extra_headers: Send extra headers @@ -95,6 +104,7 @@ def create( "target": target, "dispatch_id": dispatch_id, "metadata": metadata, + "method": method, "options": options, "signals": signals, }, @@ -177,6 +187,7 @@ async def create( target: verification_create_params.Target, dispatch_id: str | NotGiven = NOT_GIVEN, metadata: verification_create_params.Metadata | NotGiven = NOT_GIVEN, + method: Literal["auto", "voice"] | NotGiven = NOT_GIVEN, options: verification_create_params.Options | NotGiven = NOT_GIVEN, signals: verification_create_params.Signals | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -201,10 +212,16 @@ async def create( metadata: The metadata for this verification. This object will be returned with every response or webhook sent that refers to this verification. + method: The method used for verifying this phone number. The 'voice' option provides an + accessible alternative for visually impaired users by delivering the + verification code through a phone call rather than a text message. It also + allows verification of landline numbers that cannot receive SMS messages. + **Coming soon.** + options: Verification options signals: The signals used for anti-fraud. For more details, refer to - [Signals](/guides/prevent-fraud#signals). + [Signals](/verify/v2/documentation/prevent-fraud#signals). extra_headers: Send extra headers @@ -221,6 +238,7 @@ async def create( "target": target, "dispatch_id": dispatch_id, "metadata": metadata, + "method": method, "options": options, "signals": signals, }, diff --git a/src/prelude_python_sdk/resources/watch.py b/src/prelude_python_sdk/resources/watch.py index 0907e8b..b1f2e82 100644 --- a/src/prelude_python_sdk/resources/watch.py +++ b/src/prelude_python_sdk/resources/watch.py @@ -2,9 +2,11 @@ from __future__ import annotations +from typing import Iterable + import httpx -from ..types import watch_predict_params, watch_feed_back_params +from ..types import watch_predict_params, watch_send_events_params, watch_send_feedbacks_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, @@ -20,7 +22,8 @@ ) from .._base_client import make_request_options from ..types.watch_predict_response import WatchPredictResponse -from ..types.watch_feed_back_response import WatchFeedBackResponse +from ..types.watch_send_events_response import WatchSendEventsResponse +from ..types.watch_send_feedbacks_response import WatchSendFeedbacksResponse __all__ = ["WatchResource", "AsyncWatchResource"] @@ -45,28 +48,32 @@ def with_streaming_response(self) -> WatchResourceWithStreamingResponse: """ return WatchResourceWithStreamingResponse(self) - def feed_back( + def predict( self, *, - feedback: watch_feed_back_params.Feedback, - target: watch_feed_back_params.Target, + target: watch_predict_params.Target, + dispatch_id: str | NotGiven = NOT_GIVEN, + metadata: watch_predict_params.Metadata | NotGiven = NOT_GIVEN, + signals: watch_predict_params.Signals | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> WatchFeedBackResponse: + ) -> WatchPredictResponse: """ - Once the user with a trustworthy phone number demonstrates authentic behavior, - call this endpoint to report their authenticity to our systems. + Predict the outcome of a verification based on Prelude’s anti-fraud system. Args: - feedback: You should send a feedback event back to Watch API when your user demonstrates - authentic behavior. + target: The prediction target. Only supports phone numbers for now. + + dispatch_id: The identifier of the dispatch that came from the front-end SDK. + + metadata: The metadata for this prediction. - 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: The signals used for anti-fraud. For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). extra_headers: Send extra headers @@ -77,43 +84,39 @@ def feed_back( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/v2/watch/feedback", + "/v2/watch/predict", body=maybe_transform( { - "feedback": feedback, "target": target, + "dispatch_id": dispatch_id, + "metadata": metadata, + "signals": signals, }, - watch_feed_back_params.WatchFeedBackParams, + watch_predict_params.WatchPredictParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WatchFeedBackResponse, + cast_to=WatchPredictResponse, ) - def predict( + def send_events( self, *, - target: watch_predict_params.Target, - signals: watch_predict_params.Signals | NotGiven = NOT_GIVEN, + events: Iterable[watch_send_events_params.Event], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> WatchPredictResponse: + ) -> WatchSendEventsResponse: """ - Identify trustworthy phone numbers to mitigate fake traffic or traffic involved - in fraud and international revenue share fraud (IRSF) patterns. This endpoint - must be implemented in conjunction with the `watch/feedback` endpoint. + Send real-time event data from end-user interactions within your application. + Events will be analyzed for proactive fraud prevention and risk scoring. Args: - 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. + events: A list of events to dispatch. extra_headers: Send extra headers @@ -124,18 +127,48 @@ def predict( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/v2/watch/predict", - body=maybe_transform( - { - "target": target, - "signals": signals, - }, - watch_predict_params.WatchPredictParams, + "/v2/watch/event", + body=maybe_transform({"events": events}, watch_send_events_params.WatchSendEventsParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), + cast_to=WatchSendEventsResponse, + ) + + def send_feedbacks( + self, + *, + feedbacks: Iterable[watch_send_feedbacks_params.Feedback], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WatchSendFeedbacksResponse: + """Send feedback regarding your end-users verification funnel. + + Events will be + analyzed for proactive fraud prevention and risk scoring. + + Args: + feedbacks: A list of feedbacks to send. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v2/watch/feedback", + body=maybe_transform({"feedbacks": feedbacks}, watch_send_feedbacks_params.WatchSendFeedbacksParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WatchPredictResponse, + cast_to=WatchSendFeedbacksResponse, ) @@ -159,28 +192,32 @@ def with_streaming_response(self) -> AsyncWatchResourceWithStreamingResponse: """ return AsyncWatchResourceWithStreamingResponse(self) - async def feed_back( + async def predict( self, *, - feedback: watch_feed_back_params.Feedback, - target: watch_feed_back_params.Target, + target: watch_predict_params.Target, + dispatch_id: str | NotGiven = NOT_GIVEN, + metadata: watch_predict_params.Metadata | NotGiven = NOT_GIVEN, + signals: watch_predict_params.Signals | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> WatchFeedBackResponse: + ) -> WatchPredictResponse: """ - Once the user with a trustworthy phone number demonstrates authentic behavior, - call this endpoint to report their authenticity to our systems. + Predict the outcome of a verification based on Prelude’s anti-fraud system. Args: - feedback: You should send a feedback event back to Watch API when your user demonstrates - authentic behavior. + target: The prediction target. Only supports phone numbers for now. + + dispatch_id: The identifier of the dispatch that came from the front-end SDK. - 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. + metadata: The metadata for this prediction. + + signals: The signals used for anti-fraud. For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). extra_headers: Send extra headers @@ -191,43 +228,75 @@ async def feed_back( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/v2/watch/feedback", + "/v2/watch/predict", body=await async_maybe_transform( { - "feedback": feedback, "target": target, + "dispatch_id": dispatch_id, + "metadata": metadata, + "signals": signals, }, - watch_feed_back_params.WatchFeedBackParams, + watch_predict_params.WatchPredictParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WatchFeedBackResponse, + cast_to=WatchPredictResponse, ) - async def predict( + async def send_events( self, *, - target: watch_predict_params.Target, - signals: watch_predict_params.Signals | NotGiven = NOT_GIVEN, + events: Iterable[watch_send_events_params.Event], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> WatchPredictResponse: + ) -> WatchSendEventsResponse: """ - Identify trustworthy phone numbers to mitigate fake traffic or traffic involved - in fraud and international revenue share fraud (IRSF) patterns. This endpoint - must be implemented in conjunction with the `watch/feedback` endpoint. + Send real-time event data from end-user interactions within your application. + Events will be analyzed for proactive fraud prevention and risk scoring. Args: - 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. + events: A list of events to dispatch. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request - signals: It is highly recommended that you provide the signals to increase prediction - performance. + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v2/watch/event", + body=await async_maybe_transform({"events": events}, watch_send_events_params.WatchSendEventsParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WatchSendEventsResponse, + ) + + async def send_feedbacks( + self, + *, + feedbacks: Iterable[watch_send_feedbacks_params.Feedback], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WatchSendFeedbacksResponse: + """Send feedback regarding your end-users verification funnel. + + Events will be + analyzed for proactive fraud prevention and risk scoring. + + Args: + feedbacks: A list of feedbacks to send. extra_headers: Send extra headers @@ -238,18 +307,14 @@ async def predict( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/v2/watch/predict", + "/v2/watch/feedback", body=await async_maybe_transform( - { - "target": target, - "signals": signals, - }, - watch_predict_params.WatchPredictParams, + {"feedbacks": feedbacks}, watch_send_feedbacks_params.WatchSendFeedbacksParams ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WatchPredictResponse, + cast_to=WatchSendFeedbacksResponse, ) @@ -257,45 +322,57 @@ class WatchResourceWithRawResponse: def __init__(self, watch: WatchResource) -> None: self._watch = watch - self.feed_back = to_raw_response_wrapper( - watch.feed_back, - ) self.predict = to_raw_response_wrapper( watch.predict, ) + self.send_events = to_raw_response_wrapper( + watch.send_events, + ) + self.send_feedbacks = to_raw_response_wrapper( + watch.send_feedbacks, + ) class AsyncWatchResourceWithRawResponse: def __init__(self, watch: AsyncWatchResource) -> None: self._watch = watch - self.feed_back = async_to_raw_response_wrapper( - watch.feed_back, - ) self.predict = async_to_raw_response_wrapper( watch.predict, ) + self.send_events = async_to_raw_response_wrapper( + watch.send_events, + ) + self.send_feedbacks = async_to_raw_response_wrapper( + watch.send_feedbacks, + ) class WatchResourceWithStreamingResponse: def __init__(self, watch: WatchResource) -> None: self._watch = watch - self.feed_back = to_streamed_response_wrapper( - watch.feed_back, - ) self.predict = to_streamed_response_wrapper( watch.predict, ) + self.send_events = to_streamed_response_wrapper( + watch.send_events, + ) + self.send_feedbacks = to_streamed_response_wrapper( + watch.send_feedbacks, + ) class AsyncWatchResourceWithStreamingResponse: def __init__(self, watch: AsyncWatchResource) -> None: self._watch = watch - self.feed_back = async_to_streamed_response_wrapper( - watch.feed_back, - ) self.predict = async_to_streamed_response_wrapper( watch.predict, ) + self.send_events = async_to_streamed_response_wrapper( + watch.send_events, + ) + self.send_feedbacks = async_to_streamed_response_wrapper( + watch.send_feedbacks, + ) diff --git a/src/prelude_python_sdk/types/__init__.py b/src/prelude_python_sdk/types/__init__.py index 9f98177..8abf857 100644 --- a/src/prelude_python_sdk/types/__init__.py +++ b/src/prelude_python_sdk/types/__init__.py @@ -2,13 +2,17 @@ from __future__ import annotations +from .lookup_lookup_params import LookupLookupParams as LookupLookupParams from .watch_predict_params import WatchPredictParams as WatchPredictParams -from .watch_feed_back_params import WatchFeedBackParams as WatchFeedBackParams +from .lookup_lookup_response import LookupLookupResponse as LookupLookupResponse from .watch_predict_response import WatchPredictResponse as WatchPredictResponse -from .watch_feed_back_response import WatchFeedBackResponse as WatchFeedBackResponse +from .watch_send_events_params import WatchSendEventsParams as WatchSendEventsParams from .transactional_send_params import TransactionalSendParams as TransactionalSendParams from .verification_check_params import VerificationCheckParams as VerificationCheckParams from .verification_create_params import VerificationCreateParams as VerificationCreateParams +from .watch_send_events_response import WatchSendEventsResponse as WatchSendEventsResponse from .transactional_send_response import TransactionalSendResponse as TransactionalSendResponse from .verification_check_response import VerificationCheckResponse as VerificationCheckResponse +from .watch_send_feedbacks_params import WatchSendFeedbacksParams as WatchSendFeedbacksParams from .verification_create_response import VerificationCreateResponse as VerificationCreateResponse +from .watch_send_feedbacks_response import WatchSendFeedbacksResponse as WatchSendFeedbacksResponse diff --git a/src/prelude_python_sdk/types/lookup_lookup_params.py b/src/prelude_python_sdk/types/lookup_lookup_params.py new file mode 100644 index 0000000..c5b99b5 --- /dev/null +++ b/src/prelude_python_sdk/types/lookup_lookup_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, TypedDict + +__all__ = ["LookupLookupParams"] + + +class LookupLookupParams(TypedDict, total=False): + type: List[Literal["cnam"]] + """Optional features. Possible values are: + + - `cnam` - Retrieve CNAM (Caller ID Name) along with other information. Contact + us if you need to use this functionality. + """ diff --git a/src/prelude_python_sdk/types/lookup_lookup_response.py b/src/prelude_python_sdk/types/lookup_lookup_response.py new file mode 100644 index 0000000..f585823 --- /dev/null +++ b/src/prelude_python_sdk/types/lookup_lookup_response.py @@ -0,0 +1,116 @@ +# 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 + +__all__ = ["LookupLookupResponse", "NetworkInfo", "OriginalNetworkInfo"] + + +class NetworkInfo(BaseModel): + carrier_name: Optional[str] = None + """The name of the carrier.""" + + mcc: Optional[str] = None + """Mobile Country Code.""" + + mnc: Optional[str] = None + """Mobile Network Code.""" + + +class OriginalNetworkInfo(BaseModel): + carrier_name: Optional[str] = None + """The name of the original carrier.""" + + mcc: Optional[str] = None + """Mobile Country Code.""" + + mnc: Optional[str] = None + """Mobile Network Code.""" + + +class LookupLookupResponse(BaseModel): + caller_name: Optional[str] = None + """The CNAM (Caller ID Name) associated with the phone number. + + Contact us if you need to use this functionality. Once enabled, put `cnam` + option to `type` query parameter. + """ + + country_code: Optional[str] = None + """The country code of the phone number.""" + + flags: Optional[List[Literal["ported", "temporary"]]] = None + """A list of flags associated with the phone number. + + - `ported` - Indicates the phone number has been transferred from one carrier to + another. + - `temporary` - Indicates the phone number is likely a temporary or virtual + number, often used for verification services or burner phones. + """ + + line_type: Optional[ + Literal[ + "calling_cards", + "fixed_line", + "isp", + "local_rate", + "mobile", + "other", + "pager", + "payphone", + "premium_rate", + "satellite", + "service", + "shared_cost", + "short_codes_commercial", + "toll_free", + "universal_access", + "unknown", + "vpn", + "voice_mail", + "voip", + ] + ] = None + """The type of phone line. + + - `calling_cards` - Numbers that are associated with providers of pre-paid + domestic and international calling cards. + - `fixed_line` - Landline phone numbers. + - `isp` - Numbers reserved for Internet Service Providers. + - `local_rate` - Numbers that can be assigned non-geographically. + - `mobile` - Mobile phone numbers. + - `other` - Other types of services. + - `pager` - Number ranges specifically allocated to paging devices. + - `payphone` - Allocated numbers for payphone kiosks in some countries. + - `premium_rate` - Landline numbers where the calling party pays more than + standard. + - `satellite` - Satellite phone numbers. + - `service` - Automated applications. + - `shared_cost` - Specific landline ranges where the cost of making the call is + shared between the calling and called party. + - `short_codes_commercial` - Short codes are memorable, easy-to-use numbers, + like the UK's NHS 111, often sold to businesses. Not available in all + countries. + - `toll_free` - Number where the called party pays for the cost of the call not + the calling party. + - `universal_access` - Number ranges reserved for Universal Access initiatives. + - `unknown` - Unknown phone number type. + - `vpn` - Numbers are used exclusively within a private telecommunications + network, connecting the operator's terminals internally and not accessible via + the public telephone network. + - `voice_mail` - A specific category of Interactive Voice Response (IVR) + services. + - `voip` - Specific ranges for providers of VoIP services to allow incoming + calls from the regular telephony network. + """ + + network_info: Optional[NetworkInfo] = None + """The current carrier information.""" + + original_network_info: Optional[OriginalNetworkInfo] = None + """The original carrier information.""" + + phone_number: Optional[str] = None + """The phone number.""" diff --git a/src/prelude_python_sdk/types/verification_create_params.py b/src/prelude_python_sdk/types/verification_create_params.py index 15a83b6..b057312 100644 --- a/src/prelude_python_sdk/types/verification_create_params.py +++ b/src/prelude_python_sdk/types/verification_create_params.py @@ -26,13 +26,23 @@ class VerificationCreateParams(TypedDict, total=False): this verification. """ + method: Literal["auto", "voice"] + """The method used for verifying this phone number. + + The 'voice' option provides an accessible alternative for visually impaired + users by delivering the verification code through a phone call rather than a + text message. It also allows verification of landline numbers that cannot + receive SMS messages. **Coming soon.** + """ + options: Options """Verification options""" signals: Signals """The signals used for anti-fraud. - For more details, refer to [Signals](/guides/prevent-fraud#signals). + For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). """ @@ -71,7 +81,7 @@ class Options(TypedDict, total=False): """ 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). + details, refer to [Webhook](/verify/v2/documentation/webhook). """ code_size: int @@ -86,7 +96,7 @@ class Options(TypedDict, total=False): This feature is only available for compatibility purposes and subject to Prelude’s approval. Contact us to discuss your use case. For more details, refer - to [Multi Routing](/concepts/multi-routing). + to [Multi Routing](/introduction/concepts/multi-routing). """ locale: str @@ -137,7 +147,8 @@ class Signals(TypedDict, total=False): is_trusted_user: bool """ This signal should provide a higher level of trust, indicating that the user is - genuine. For more details, refer to [Signals](/guides/prevent-fraud#signals). + genuine. For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). """ os_version: str diff --git a/src/prelude_python_sdk/types/watch_feed_back_params.py b/src/prelude_python_sdk/types/watch_feed_back_params.py deleted file mode 100644 index c4cc330..0000000 --- a/src/prelude_python_sdk/types/watch_feed_back_params.py +++ /dev/null @@ -1,38 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["WatchFeedBackParams", "Feedback", "Target"] - - -class WatchFeedBackParams(TypedDict, total=False): - feedback: Required[Feedback] - """ - You should send a feedback event back to Watch API when your user demonstrates - authentic behavior. - """ - - target: Required[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. - """ - - -class Feedback(TypedDict, total=False): - type: Required[Literal["CONFIRM_TARGET"]] - """ - `CONFIRM_TARGET` should be sent when you are sure that the user with this target - (e.g. phone number) is trustworthy. - """ - - -class Target(TypedDict, total=False): - 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 or an email address.""" diff --git a/src/prelude_python_sdk/types/watch_feed_back_response.py b/src/prelude_python_sdk/types/watch_feed_back_response.py deleted file mode 100644 index c879860..0000000 --- a/src/prelude_python_sdk/types/watch_feed_back_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - - -from .._models import BaseModel - -__all__ = ["WatchFeedBackResponse"] - - -class WatchFeedBackResponse(BaseModel): - id: str - """A unique identifier for your feedback request.""" diff --git a/src/prelude_python_sdk/types/watch_predict_params.py b/src/prelude_python_sdk/types/watch_predict_params.py index 6ef2093..d0b98b8 100644 --- a/src/prelude_python_sdk/types/watch_predict_params.py +++ b/src/prelude_python_sdk/types/watch_predict_params.py @@ -4,21 +4,24 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["WatchPredictParams", "Target", "Signals"] +__all__ = ["WatchPredictParams", "Target", "Metadata", "Signals"] class WatchPredictParams(TypedDict, total=False): target: Required[Target] - """The verification target. + """The prediction target. Only supports phone numbers for now.""" - 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.""" + + metadata: Metadata + """The metadata for this prediction.""" signals: Signals - """ - It is highly recommended that you provide the signals to increase prediction - performance. + """The signals used for anti-fraud. + + For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). """ @@ -30,7 +33,15 @@ class Target(TypedDict, total=False): """An E.164 formatted phone number or an email address.""" +class Metadata(TypedDict, total=False): + correlation_id: str + """A user-defined identifier to correlate this prediction with.""" + + class Signals(TypedDict, total=False): + app_version: str + """The version of your application.""" + device_id: str """The unique identifier for the user's device. @@ -41,8 +52,26 @@ class Signals(TypedDict, total=False): device_model: str """The model of the user's device.""" - device_type: str + device_platform: Literal["android", "ios", "ipados", "tvos", "web"] """The type of the user's device.""" ip: str - """The IPv4 address of the user's device""" + """The IP address of the user's device.""" + + is_trusted_user: bool + """ + This signal should provide a higher level of trust, indicating that the user is + genuine. For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). + """ + + 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_predict_response.py b/src/prelude_python_sdk/types/watch_predict_response.py index cb5a871..0bd330c 100644 --- a/src/prelude_python_sdk/types/watch_predict_response.py +++ b/src/prelude_python_sdk/types/watch_predict_response.py @@ -1,29 +1,21 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional from typing_extensions import Literal from .._models import BaseModel -__all__ = ["WatchPredictResponse", "Reasoning"] - - -class Reasoning(BaseModel): - cause: Optional[Literal["none", "smart_antifraud", "repeat_number", "invalid_line"]] = None - """A label explaining why the phone number was classified as not trustworthy""" - - score: Optional[float] = None - """ - Indicates the risk of the phone number being genuine or involved in fraudulent - patterns. The higher the riskier. - """ +__all__ = ["WatchPredictResponse"] class WatchPredictResponse(BaseModel): id: str - """A unique identifier for your prediction request.""" + """The prediction identifier.""" + + prediction: Literal["legitimate", "suspicious"] + """The prediction outcome.""" - prediction: Literal["allow", "block"] - """A label indicating the trustworthiness of the phone number.""" + request_id: str + """A string that identifies this specific request. - reasoning: Reasoning + Report it back to us to help us diagnose your issues. + """ diff --git a/src/prelude_python_sdk/types/watch_send_events_params.py b/src/prelude_python_sdk/types/watch_send_events_params.py new file mode 100644 index 0000000..ab5b492 --- /dev/null +++ b/src/prelude_python_sdk/types/watch_send_events_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["WatchSendEventsParams", "Event", "EventTarget"] + + +class WatchSendEventsParams(TypedDict, total=False): + events: Required[Iterable[Event]] + """A list of events to dispatch.""" + + +class EventTarget(TypedDict, total=False): + 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 or an email address.""" + + +class Event(TypedDict, total=False): + confidence: Required[Literal["maximum", "high", "neutral", "low", "minimum"]] + """A confidence level you want to assign to the event.""" + + label: Required[str] + """A label to describe what the event refers to.""" + + target: Required[EventTarget] + """The event target. Only supports phone numbers for now.""" diff --git a/src/prelude_python_sdk/types/watch_send_events_response.py b/src/prelude_python_sdk/types/watch_send_events_response.py new file mode 100644 index 0000000..dab5729 --- /dev/null +++ b/src/prelude_python_sdk/types/watch_send_events_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["WatchSendEventsResponse"] + + +class WatchSendEventsResponse(BaseModel): + request_id: str + """A string that identifies this specific request. + + Report it back to us to help us diagnose your issues. + """ + + status: Literal["success"] + """The status of the events dispatch.""" diff --git a/src/prelude_python_sdk/types/watch_send_feedbacks_params.py b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py new file mode 100644 index 0000000..4320e60 --- /dev/null +++ b/src/prelude_python_sdk/types/watch_send_feedbacks_params.py @@ -0,0 +1,86 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["WatchSendFeedbacksParams", "Feedback", "FeedbackTarget", "FeedbackMetadata", "FeedbackSignals"] + + +class WatchSendFeedbacksParams(TypedDict, total=False): + feedbacks: Required[Iterable[Feedback]] + """A list of feedbacks to send.""" + + +class FeedbackTarget(TypedDict, total=False): + 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 or an email address.""" + + +class FeedbackMetadata(TypedDict, total=False): + correlation_id: str + """A user-defined identifier to correlate this feedback with.""" + + +class FeedbackSignals(TypedDict, total=False): + app_version: str + """The version of your application.""" + + device_id: str + """The unique identifier for the user's device. + + 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 IP address of the user's device.""" + + is_trusted_user: bool + """ + This signal should provide a higher level of trust, indicating that the user is + genuine. For more details, refer to + [Signals](/verify/v2/documentation/prevent-fraud#signals). + """ + + 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.""" + + 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). + """ diff --git a/src/prelude_python_sdk/types/watch_send_feedbacks_response.py b/src/prelude_python_sdk/types/watch_send_feedbacks_response.py new file mode 100644 index 0000000..ca05636 --- /dev/null +++ b/src/prelude_python_sdk/types/watch_send_feedbacks_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["WatchSendFeedbacksResponse"] + + +class WatchSendFeedbacksResponse(BaseModel): + request_id: str + """A string that identifies this specific request. + + Report it back to us to help us diagnose your issues. + """ + + status: Literal["success"] + """The status of the feedbacks sending.""" diff --git a/tests/api_resources/test_lookup.py b/tests/api_resources/test_lookup.py new file mode 100644 index 0000000..c65a5c2 --- /dev/null +++ b/tests/api_resources/test_lookup.py @@ -0,0 +1,114 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from prelude_python_sdk import Prelude, AsyncPrelude +from prelude_python_sdk.types import LookupLookupResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLookup: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_lookup(self, client: Prelude) -> None: + lookup = client.lookup.lookup( + phone_number="+12065550100", + ) + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + def test_method_lookup_with_all_params(self, client: Prelude) -> None: + lookup = client.lookup.lookup( + phone_number="+12065550100", + type=["cnam"], + ) + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + def test_raw_response_lookup(self, client: Prelude) -> None: + response = client.lookup.with_raw_response.lookup( + phone_number="+12065550100", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = response.parse() + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + def test_streaming_response_lookup(self, client: Prelude) -> None: + with client.lookup.with_streaming_response.lookup( + phone_number="+12065550100", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = response.parse() + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_lookup(self, client: Prelude) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `phone_number` but received ''"): + client.lookup.with_raw_response.lookup( + phone_number="", + ) + + +class TestAsyncLookup: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_lookup(self, async_client: AsyncPrelude) -> None: + lookup = await async_client.lookup.lookup( + phone_number="+12065550100", + ) + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + async def test_method_lookup_with_all_params(self, async_client: AsyncPrelude) -> None: + lookup = await async_client.lookup.lookup( + phone_number="+12065550100", + type=["cnam"], + ) + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + async def test_raw_response_lookup(self, async_client: AsyncPrelude) -> None: + response = await async_client.lookup.with_raw_response.lookup( + phone_number="+12065550100", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = await response.parse() + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + @parametrize + async def test_streaming_response_lookup(self, async_client: AsyncPrelude) -> None: + async with async_client.lookup.with_streaming_response.lookup( + phone_number="+12065550100", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = await response.parse() + assert_matches_type(LookupLookupResponse, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_lookup(self, async_client: AsyncPrelude) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `phone_number` but received ''"): + await async_client.lookup.with_raw_response.lookup( + phone_number="", + ) diff --git a/tests/api_resources/test_verification.py b/tests/api_resources/test_verification.py index d88eca4..015cd08 100644 --- a/tests/api_resources/test_verification.py +++ b/tests/api_resources/test_verification.py @@ -39,6 +39,7 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: }, dispatch_id="dispatch_id", metadata={"correlation_id": "correlation_id"}, + method="auto", options={ "app_realm": { "platform": "android", @@ -56,7 +57,7 @@ def test_method_create_with_all_params(self, client: Prelude) -> None: "app_version": "1.2.34", "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", - "device_platform": "android", + "device_platform": "ios", "ip": "192.0.2.1", "is_trusted_user": False, "os_version": "18.0.1", @@ -161,6 +162,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - }, dispatch_id="dispatch_id", metadata={"correlation_id": "correlation_id"}, + method="auto", options={ "app_realm": { "platform": "android", @@ -178,7 +180,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncPrelude) - "app_version": "1.2.34", "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", "device_model": "iPhone17,2", - "device_platform": "android", + "device_platform": "ios", "ip": "192.0.2.1", "is_trusted_user": False, "os_version": "18.0.1", diff --git a/tests/api_resources/test_watch.py b/tests/api_resources/test_watch.py index 1e948da..80096e0 100644 --- a/tests/api_resources/test_watch.py +++ b/tests/api_resources/test_watch.py @@ -11,7 +11,8 @@ from prelude_python_sdk import Prelude, AsyncPrelude from prelude_python_sdk.types import ( WatchPredictResponse, - WatchFeedBackResponse, + WatchSendEventsResponse, + WatchSendFeedbacksResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,49 +21,6 @@ class TestWatch: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - def test_method_feed_back(self, client: Prelude) -> None: - watch = client.watch.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, - ) - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) - - @parametrize - def test_raw_response_feed_back(self, client: Prelude) -> None: - response = client.watch.with_raw_response.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - watch = response.parse() - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) - - @parametrize - def test_streaming_response_feed_back(self, client: Prelude) -> None: - with client.watch.with_streaming_response.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - watch = response.parse() - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize def test_method_predict(self, client: Prelude) -> None: watch = client.watch.predict( @@ -80,11 +38,17 @@ def test_method_predict_with_all_params(self, client: Prelude) -> None: "type": "phone_number", "value": "+30123456789", }, + dispatch_id="dispatch_id", + metadata={"correlation_id": "correlation_id"}, signals={ - "device_id": "device_id", - "device_model": "device_model", - "device_type": "device_type", - "ip": "ip", + "app_version": "1.2.34", + "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", + "device_model": "iPhone17,2", + "device_platform": "ios", + "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(WatchPredictResponse, watch, path=["response"]) @@ -119,53 +83,123 @@ def test_streaming_response_predict(self, client: Prelude) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_send_events(self, client: Prelude) -> None: + watch = client.watch.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) -class TestAsyncWatch: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_raw_response_send_events(self, client: Prelude) -> None: + response = client.watch.with_raw_response.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = response.parse() + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) @parametrize - async def test_method_feed_back(self, async_client: AsyncPrelude) -> None: - watch = await async_client.watch.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, + def test_streaming_response_send_events(self, client: Prelude) -> None: + with client.watch.with_streaming_response.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = response.parse() + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_send_feedbacks(self, client: Prelude) -> None: + watch = client.watch.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], ) - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) @parametrize - async def test_raw_response_feed_back(self, async_client: AsyncPrelude) -> None: - response = await async_client.watch.with_raw_response.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, + def test_raw_response_send_feedbacks(self, client: Prelude) -> None: + response = client.watch.with_raw_response.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - watch = await response.parse() - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) + watch = response.parse() + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) @parametrize - async def test_streaming_response_feed_back(self, async_client: AsyncPrelude) -> None: - async with async_client.watch.with_streaming_response.feed_back( - feedback={"type": "CONFIRM_TARGET"}, - target={ - "type": "phone_number", - "value": "+30123456789", - }, + def test_streaming_response_send_feedbacks(self, client: Prelude) -> None: + with client.watch.with_streaming_response.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - watch = await response.parse() - assert_matches_type(WatchFeedBackResponse, watch, path=["response"]) + watch = response.parse() + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) assert cast(Any, response.is_closed) is True + +class TestAsyncWatch: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize async def test_method_predict(self, async_client: AsyncPrelude) -> None: watch = await async_client.watch.predict( @@ -183,11 +217,17 @@ async def test_method_predict_with_all_params(self, async_client: AsyncPrelude) "type": "phone_number", "value": "+30123456789", }, + dispatch_id="dispatch_id", + metadata={"correlation_id": "correlation_id"}, signals={ - "device_id": "device_id", - "device_model": "device_model", - "device_type": "device_type", - "ip": "ip", + "app_version": "1.2.34", + "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", + "device_model": "iPhone17,2", + "device_platform": "ios", + "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(WatchPredictResponse, watch, path=["response"]) @@ -221,3 +261,116 @@ async def test_streaming_response_predict(self, async_client: AsyncPrelude) -> N assert_matches_type(WatchPredictResponse, watch, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_send_events(self, async_client: AsyncPrelude) -> None: + watch = await async_client.watch.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) + + @parametrize + async def test_raw_response_send_events(self, async_client: AsyncPrelude) -> None: + response = await async_client.watch.with_raw_response.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = await response.parse() + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) + + @parametrize + async def test_streaming_response_send_events(self, async_client: AsyncPrelude) -> None: + async with async_client.watch.with_streaming_response.send_events( + events=[ + { + "confidence": "maximum", + "label": "onboarding.start", + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = await response.parse() + assert_matches_type(WatchSendEventsResponse, watch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_send_feedbacks(self, async_client: AsyncPrelude) -> None: + watch = await async_client.watch.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], + ) + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) + + @parametrize + async def test_raw_response_send_feedbacks(self, async_client: AsyncPrelude) -> None: + response = await async_client.watch.with_raw_response.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + watch = await response.parse() + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) + + @parametrize + async def test_streaming_response_send_feedbacks(self, async_client: AsyncPrelude) -> None: + async with async_client.watch.with_streaming_response.send_feedbacks( + feedbacks=[ + { + "target": { + "type": "phone_number", + "value": "+30123456789", + }, + "type": "verification.started", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + watch = await response.parse() + assert_matches_type(WatchSendFeedbacksResponse, watch, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index a9d9e60..6303f15 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1703,7 +1703,7 @@ def test_get_platform(self) -> None: import threading from prelude_python_sdk._utils import asyncify - from prelude_python_sdk._base_client import get_platform + from prelude_python_sdk._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() diff --git a/tests/test_models.py b/tests/test_models.py index 638d33f..2a5225a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -854,3 +854,35 @@ class Model(BaseModel): m = construct_type(value={"cls": "foo"}, type_=Model) assert isinstance(m, Model) assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) diff --git a/tests/test_transform.py b/tests/test_transform.py index b48736d..16f6a21 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from prelude_python_sdk._types import Base64FileInput +from prelude_python_sdk._types import NOT_GIVEN, Base64FileInput from prelude_python_sdk._utils import ( PropertyInfo, transform as _transform, @@ -432,3 +432,22 @@ async def test_base64_file_input(use_async: bool) -> None: assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { "foo": "SGVsbG8sIHdvcmxkIQ==" } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}