diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 91946882..19c0e9cc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.103.1" + ".": "0.104.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 8791c52a..1d2ac3a5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 168 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-e16df7d65a6ababc8e0ca1f2a65070893d82d3b2b046394ab708d56fe717b3ad.yml -openapi_spec_hash: ee82cf8fd5bb6b86abbae304f6c43934 -config_hash: c6d56596249e319c59d49d8e49a190b1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-117e0ee9b030a2efc3b09e189e445fb1a26fd32f1c563f385b9d7071a959c550.yml +openapi_spec_hash: e529a3fa8c3a79d3664db391683334c3 +config_hash: 22e4b128e110e2767daa9d95428ebf9d diff --git a/CHANGELOG.md b/CHANGELOG.md index 09766f21..e6b5f6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.104.0 (2025-09-04) + +Full Changelog: [v0.103.1...v0.104.0](https://github.com/lithic-com/lithic-python/compare/v0.103.1...v0.104.0) + +### Features + +* **api:** adds support for delegated KYB onboarding and more device details in 3DS Authentications ([0a5a2ba](https://github.com/lithic-com/lithic-python/commit/0a5a2badb3cba976896e2d34cab0c7b70a73139c)) +* improve future compat with pydantic v3 ([d589749](https://github.com/lithic-com/lithic-python/commit/d5897496aa90a50d98ac3306ca88b301a891e063)) +* **types:** replace List[str] with SequenceNotStr in params ([fa51159](https://github.com/lithic-com/lithic-python/commit/fa51159008addf6e0981eba26f71724a1e2aa8ae)) + + +### Chores + +* **internal:** add Sequence related utils ([b448cd0](https://github.com/lithic-com/lithic-python/commit/b448cd0d12bd7223622949cd5cf39ff01e9505fd)) +* **internal:** fix mypy ([#1575](https://github.com/lithic-com/lithic-python/issues/1575)) ([be186c8](https://github.com/lithic-com/lithic-python/commit/be186c8c990290873eeb1f7272278082ce391e05)) +* **internal:** minor formatting change ([0fd12bf](https://github.com/lithic-com/lithic-python/commit/0fd12bf85297db62f7d292d617226ef654583513)) +* **internal:** update pyright exclude list ([cdbdc72](https://github.com/lithic-com/lithic-python/commit/cdbdc724ffe1bf7bed3ad78f1c11241308c54a69)) + ## 0.103.1 (2025-08-26) Full Changelog: [v0.103.0...v0.103.1](https://github.com/lithic-com/lithic-python/compare/v0.103.0...v0.103.1) diff --git a/api.md b/api.md index c6fb2d1d..952f9be2 100644 --- a/api.md +++ b/api.md @@ -772,7 +772,11 @@ Methods: Types: ```python -from lithic.types import AccountActivityListResponse, AccountActivityRetrieveTransactionResponse +from lithic.types import ( + WirePartyDetails, + AccountActivityListResponse, + AccountActivityRetrieveTransactionResponse, +) ``` Methods: diff --git a/pyproject.toml b/pyproject.toml index 2a42926a..e63d0230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lithic" -version = "0.103.1" +version = "0.104.0" description = "The official Python library for the lithic API" dynamic = ["readme"] license = "Apache-2.0" @@ -148,6 +148,7 @@ exclude = [ "_dev", ".venv", ".nox", + ".git", ] reportImplicitOverride = true diff --git a/src/lithic/_base_client.py b/src/lithic/_base_client.py index 50679a45..94f2e8a0 100644 --- a/src/lithic/_base_client.py +++ b/src/lithic/_base_client.py @@ -59,7 +59,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._compat import PYDANTIC_V1, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -233,7 +233,7 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model @@ -321,7 +321,7 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model diff --git a/src/lithic/_compat.py b/src/lithic/_compat.py index 92d9ee61..bdef67f0 100644 --- a/src/lithic/_compat.py +++ b/src/lithic/_compat.py @@ -12,14 +12,13 @@ _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) -# --------------- Pydantic v2 compatibility --------------- +# --------------- Pydantic v2, v3 compatibility --------------- # Pyright incorrectly reports some of our functions as overriding a method when they don't # pyright: reportIncompatibleMethodOverride=false -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -# v1 re-exports if TYPE_CHECKING: def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 @@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 ... else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( get_args as get_args, is_union as is_union, get_origin as get_origin, is_typeddict as is_typeddict, is_literal_type as is_literal_type, ) - from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime else: - from pydantic.typing import ( + from ._utils import ( get_args as get_args, is_union as is_union, get_origin as get_origin, + parse_date as parse_date, is_typeddict as is_typeddict, + parse_datetime as parse_datetime, is_literal_type as is_literal_type, ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime # refactored config if TYPE_CHECKING: from pydantic import ConfigDict as ConfigDict else: - if PYDANTIC_V2: - from pydantic import ConfigDict - else: + if PYDANTIC_V1: # TODO: provide an error message here? ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict # renamed methods / properties def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(value) - else: + if PYDANTIC_V1: return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V2: - return field.is_required() - return field.required # type: ignore + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() def field_get_default(field: FieldInfo) -> Any: value = field.get_default() - if PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None + if PYDANTIC_V1: return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None return value def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V2: - return field.annotation - return field.outer_type_ # type: ignore + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V2: - return model.model_fields - return model.__fields__ # type: ignore + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) def model_dump( @@ -139,14 +140,14 @@ def model_dump( warnings: bool = True, mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 - warnings=warnings if PYDANTIC_V2 else True, + warnings=True if PYDANTIC_V1 else warnings, ) return cast( "dict[str, Any]", @@ -159,9 +160,9 @@ def model_dump( def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) # generic models @@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: class GenericModel(pydantic.BaseModel): ... else: - if PYDANTIC_V2: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors class GenericModel(pydantic.BaseModel): ... - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - # cached properties if TYPE_CHECKING: diff --git a/src/lithic/_models.py b/src/lithic/_models.py index 92f7c10b..3a6017ef 100644 --- a/src/lithic/_models.py +++ b/src/lithic/_models.py @@ -50,7 +50,7 @@ strip_annotated_type, ) from ._compat import ( - PYDANTIC_V2, + PYDANTIC_V1, ConfigDict, GenericModel as BaseGenericModel, get_args, @@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol): class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: + if PYDANTIC_V1: @property @override @@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) def to_dict( self, @@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if key not in model_fields: parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - if PYDANTIC_V2: - _extra[key] = parsed - else: + if PYDANTIC_V1: _fields_set.add(key) fields_values[key] = parsed + else: + _extra[key] = parsed object.__setattr__(m, "__dict__", fields_values) - if PYDANTIC_V2: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - else: + if PYDANTIC_V1: # init_private_attributes() does not exist in v2 m._init_private_attributes() # type: ignore # copied from Pydantic v1's `construct()` method object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) return m @@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # although not in practice model_construct = construct - if not PYDANTIC_V2: + if PYDANTIC_V1: # we define aliases for some of the new pydantic v2 methods so # that we can just document these methods without having to specify # a specific pydantic version as some users may not know which @@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) - if PYDANTIC_V2: - type_ = field.annotation - else: + if PYDANTIC_V1: type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") @@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: + if PYDANTIC_V1: # TODO return None @@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, for variant in get_args(union): variant = strip_annotated_type(variant) if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] + discriminator_alias = field_info.alias - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant else: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias + discriminator_alias = field.get("serialization_alias") - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant @@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel): pass -if PYDANTIC_V2: +if not PYDANTIC_V1: from pydantic import TypeAdapter as _TypeAdapter _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) @@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel): json_data: Union[Body, None] = None extra_json: Union[AnyMapping, None] = None - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: + if PYDANTIC_V1: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) def get_max_retries(self, max_retries: int) -> int: if isinstance(self.max_retries, NotGiven): @@ -820,9 +820,9 @@ def construct( # type: ignore key: strip_not_given(value) for key, value in values.items() } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) if not TYPE_CHECKING: # type checkers incorrectly complain about this assignment diff --git a/src/lithic/_types.py b/src/lithic/_types.py index 8100439c..933af6d5 100644 --- a/src/lithic/_types.py +++ b/src/lithic/_types.py @@ -13,10 +13,21 @@ Mapping, TypeVar, Callable, + Iterator, Optional, Sequence, ) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) import httpx import pydantic @@ -219,3 +230,26 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... + def count(self, value: Any, /) -> int: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/lithic/_utils/__init__.py b/src/lithic/_utils/__init__.py index d4fda26f..dc64e29a 100644 --- a/src/lithic/_utils/__init__.py +++ b/src/lithic/_utils/__init__.py @@ -10,7 +10,6 @@ lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, - parse_date as parse_date, is_iterable as is_iterable, is_sequence as is_sequence, coerce_float as coerce_float, @@ -23,7 +22,6 @@ coerce_boolean as coerce_boolean, coerce_integer as coerce_integer, file_from_path as file_from_path, - parse_datetime as parse_datetime, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,12 +30,20 @@ maybe_coerce_boolean as maybe_coerce_boolean, maybe_coerce_integer as maybe_coerce_integer, ) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) from ._typing import ( is_list_type as is_list_type, is_union_type as is_union_type, extract_type_arg as extract_type_arg, is_iterable_type as is_iterable_type, is_required_type as is_required_type, + is_sequence_type as is_sequence_type, is_annotated_type as is_annotated_type, is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, @@ -55,3 +61,4 @@ function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, ) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/lithic/_utils/_compat.py b/src/lithic/_utils/_compat.py new file mode 100644 index 00000000..dd703233 --- /dev/null +++ b/src/lithic/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/lithic/_utils/_datetime_parse.py b/src/lithic/_utils/_datetime_parse.py new file mode 100644 index 00000000..7cb9d9e6 --- /dev/null +++ b/src/lithic/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/lithic/_utils/_transform.py b/src/lithic/_utils/_transform.py index b0cc20a7..c19124f0 100644 --- a/src/lithic/_utils/_transform.py +++ b/src/lithic/_utils/_transform.py @@ -16,18 +16,20 @@ lru_cache, is_mapping, is_iterable, + is_sequence, ) from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict from ._typing import ( is_list_type, is_union_type, extract_type_arg, is_iterable_type, is_required_type, + is_sequence_type, is_annotated_type, strip_annotated_type, ) -from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -167,6 +169,8 @@ def _transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -184,6 +188,8 @@ def _transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. @@ -329,6 +335,8 @@ async def _async_transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -346,6 +354,8 @@ async def _async_transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. diff --git a/src/lithic/_utils/_typing.py b/src/lithic/_utils/_typing.py index 1bac9542..193109f3 100644 --- a/src/lithic/_utils/_typing.py +++ b/src/lithic/_utils/_typing.py @@ -15,7 +15,7 @@ from ._utils import lru_cache from .._types import InheritsGeneric -from .._compat import is_union as _is_union +from ._compat import is_union as _is_union def is_annotated_type(typ: type) -> bool: @@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool: return (get_origin(typ) or typ) == list +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + def is_iterable_type(typ: type) -> bool: """If the given type is `typing.Iterable[T]`""" origin = get_origin(typ) or typ diff --git a/src/lithic/_utils/_utils.py b/src/lithic/_utils/_utils.py index ea3cf3f2..f0818595 100644 --- a/src/lithic/_utils/_utils.py +++ b/src/lithic/_utils/_utils.py @@ -22,7 +22,6 @@ import sniffio from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) diff --git a/src/lithic/_version.py b/src/lithic/_version.py index b2e699dc..eecc58c0 100644 --- a/src/lithic/_version.py +++ b/src/lithic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "lithic" -__version__ = "0.103.1" # x-release-please-version +__version__ = "0.104.0" # x-release-please-version diff --git a/src/lithic/resources/account_holders.py b/src/lithic/resources/account_holders.py index 728609a7..6e3242ad 100644 --- a/src/lithic/resources/account_holders.py +++ b/src/lithic/resources/account_holders.py @@ -17,7 +17,7 @@ account_holder_simulate_enrollment_review_params, account_holder_simulate_enrollment_document_review_params, ) -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from .._utils import is_given, required_args, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -140,6 +140,79 @@ def create( """ ... + @overload + def create( + self, + *, + business_entity: account_holder_create_params.KYBDelegatedBusinessEntity, + beneficial_owner_individuals: Iterable[account_holder_create_params.KYBDelegatedBeneficialOwnerIndividual] + | NotGiven = NOT_GIVEN, + control_person: account_holder_create_params.KYBDelegatedControlPerson | NotGiven = NOT_GIVEN, + external_id: str | NotGiven = NOT_GIVEN, + nature_of_business: str | NotGiven = NOT_GIVEN, + tos_timestamp: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + workflow: Literal["KYB_DELEGATED"] | 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, + ) -> AccountHolderCreateResponse: + """ + Create an account holder and initiate the appropriate onboarding workflow. + Account holders and accounts have a 1:1 relationship. When an account holder is + successfully created an associated account is also created. All calls to this + endpoint will return a synchronous response. The response time will depend on + the workflow. In some cases, the response may indicate the workflow is under + review or further action will be needed to complete the account creation + process. This endpoint can only be used on accounts that are part of the program + that the calling API key manages. + + Args: + business_entity: Information for business for which the account is being opened. + + beneficial_owner_individuals: You can submit a list of all direct and indirect individuals with 25% or more + ownership in the company. A maximum of 4 beneficial owners can be submitted. If + no individual owns 25% of the company you do not need to send beneficial owner + information. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section I) for more background on individuals that should be included. + + control_person: An individual with significant responsibility for managing the legal entity + (e.g., a Chief Executive Officer, Chief Financial Officer, Chief Operating + Officer, Managing Member, General Partner, President, Vice President, or + Treasurer). This can be an executive, or someone who will have program-wide + access to the cards that Lithic will provide. In some cases, this individual + could also be a beneficial owner listed above. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section II) for more background. + + external_id: A user provided id that can be used to link an account holder with an external + system + + nature_of_business: Short description of the company's line of business (i.e., what does the company + do?). + + tos_timestamp: An RFC 3339 timestamp indicating when the account holder accepted the applicable + legal agreements (e.g., cardholder terms) as agreed upon during API customer's + implementation with Lithic. + + website_url: Company website URL. + + workflow: Specifies the type of KYB workflow to run. + + 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 + """ + ... + @overload def create( self, @@ -266,6 +339,7 @@ def create( "tos_timestamp", "workflow", ], + ["business_entity"], ["individual", "tos_timestamp", "workflow"], ["address", "email", "first_name", "kyc_exemption_type", "last_name", "phone_number", "workflow"], ) @@ -274,11 +348,17 @@ def create( *, beneficial_owner_individuals: Iterable[account_holder_create_params.KYBBeneficialOwnerIndividual] | NotGiven = NOT_GIVEN, - business_entity: account_holder_create_params.KYBBusinessEntity | NotGiven = NOT_GIVEN, + business_entity: account_holder_create_params.KYBBusinessEntity + | account_holder_create_params.KYBDelegatedBusinessEntity + | NotGiven = NOT_GIVEN, control_person: account_holder_create_params.KYBControlPerson | NotGiven = NOT_GIVEN, nature_of_business: str | NotGiven = NOT_GIVEN, tos_timestamp: str | NotGiven = NOT_GIVEN, - workflow: Literal["KYB_BASIC", "KYB_BYO"] | Literal["KYC_BASIC", "KYC_BYO"] | Literal["KYC_EXEMPT"], + workflow: Literal["KYB_BASIC", "KYB_BYO"] + | Literal["KYB_DELEGATED"] + | Literal["KYC_BASIC", "KYC_BYO"] + | Literal["KYC_EXEMPT"] + | NotGiven = NOT_GIVEN, beneficial_owner_entities: Iterable[account_holder_create_params.KYBBeneficialOwnerEntity] | NotGiven = NOT_GIVEN, external_id: str | NotGiven = NOT_GIVEN, @@ -809,7 +889,7 @@ def simulate_enrollment_document_review( *, document_upload_token: str, status: Literal["UPLOADED", "ACCEPTED", "REJECTED", "PARTIAL_APPROVAL"], - accepted_entity_status_reasons: List[str] | NotGiven = NOT_GIVEN, + accepted_entity_status_reasons: SequenceNotStr[str] | NotGiven = NOT_GIVEN, status_reason: Literal[ "DOCUMENT_MISSING_REQUIRED_DATA", "DOCUMENT_UPLOAD_TOO_BLURRY", @@ -1129,6 +1209,79 @@ async def create( """ ... + @overload + async def create( + self, + *, + business_entity: account_holder_create_params.KYBDelegatedBusinessEntity, + beneficial_owner_individuals: Iterable[account_holder_create_params.KYBDelegatedBeneficialOwnerIndividual] + | NotGiven = NOT_GIVEN, + control_person: account_holder_create_params.KYBDelegatedControlPerson | NotGiven = NOT_GIVEN, + external_id: str | NotGiven = NOT_GIVEN, + nature_of_business: str | NotGiven = NOT_GIVEN, + tos_timestamp: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + workflow: Literal["KYB_DELEGATED"] | 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, + ) -> AccountHolderCreateResponse: + """ + Create an account holder and initiate the appropriate onboarding workflow. + Account holders and accounts have a 1:1 relationship. When an account holder is + successfully created an associated account is also created. All calls to this + endpoint will return a synchronous response. The response time will depend on + the workflow. In some cases, the response may indicate the workflow is under + review or further action will be needed to complete the account creation + process. This endpoint can only be used on accounts that are part of the program + that the calling API key manages. + + Args: + business_entity: Information for business for which the account is being opened. + + beneficial_owner_individuals: You can submit a list of all direct and indirect individuals with 25% or more + ownership in the company. A maximum of 4 beneficial owners can be submitted. If + no individual owns 25% of the company you do not need to send beneficial owner + information. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section I) for more background on individuals that should be included. + + control_person: An individual with significant responsibility for managing the legal entity + (e.g., a Chief Executive Officer, Chief Financial Officer, Chief Operating + Officer, Managing Member, General Partner, President, Vice President, or + Treasurer). This can be an executive, or someone who will have program-wide + access to the cards that Lithic will provide. In some cases, this individual + could also be a beneficial owner listed above. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section II) for more background. + + external_id: A user provided id that can be used to link an account holder with an external + system + + nature_of_business: Short description of the company's line of business (i.e., what does the company + do?). + + tos_timestamp: An RFC 3339 timestamp indicating when the account holder accepted the applicable + legal agreements (e.g., cardholder terms) as agreed upon during API customer's + implementation with Lithic. + + website_url: Company website URL. + + workflow: Specifies the type of KYB workflow to run. + + 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 + """ + ... + @overload async def create( self, @@ -1255,6 +1408,7 @@ async def create( "tos_timestamp", "workflow", ], + ["business_entity"], ["individual", "tos_timestamp", "workflow"], ["address", "email", "first_name", "kyc_exemption_type", "last_name", "phone_number", "workflow"], ) @@ -1263,11 +1417,17 @@ async def create( *, beneficial_owner_individuals: Iterable[account_holder_create_params.KYBBeneficialOwnerIndividual] | NotGiven = NOT_GIVEN, - business_entity: account_holder_create_params.KYBBusinessEntity | NotGiven = NOT_GIVEN, + business_entity: account_holder_create_params.KYBBusinessEntity + | account_holder_create_params.KYBDelegatedBusinessEntity + | NotGiven = NOT_GIVEN, control_person: account_holder_create_params.KYBControlPerson | NotGiven = NOT_GIVEN, nature_of_business: str | NotGiven = NOT_GIVEN, tos_timestamp: str | NotGiven = NOT_GIVEN, - workflow: Literal["KYB_BASIC", "KYB_BYO"] | Literal["KYC_BASIC", "KYC_BYO"] | Literal["KYC_EXEMPT"], + workflow: Literal["KYB_BASIC", "KYB_BYO"] + | Literal["KYB_DELEGATED"] + | Literal["KYC_BASIC", "KYC_BYO"] + | Literal["KYC_EXEMPT"] + | NotGiven = NOT_GIVEN, beneficial_owner_entities: Iterable[account_holder_create_params.KYBBeneficialOwnerEntity] | NotGiven = NOT_GIVEN, external_id: str | NotGiven = NOT_GIVEN, @@ -1798,7 +1958,7 @@ async def simulate_enrollment_document_review( *, document_upload_token: str, status: Literal["UPLOADED", "ACCEPTED", "REJECTED", "PARTIAL_APPROVAL"], - accepted_entity_status_reasons: List[str] | NotGiven = NOT_GIVEN, + accepted_entity_status_reasons: SequenceNotStr[str] | NotGiven = NOT_GIVEN, status_reason: Literal[ "DOCUMENT_MISSING_REQUIRED_DATA", "DOCUMENT_UPLOAD_TOO_BLURRY", diff --git a/src/lithic/resources/auth_rules/v2/v2.py b/src/lithic/resources/auth_rules/v2/v2.py index c20e3eca..cb0d8182 100644 --- a/src/lithic/resources/auth_rules/v2/v2.py +++ b/src/lithic/resources/auth_rules/v2/v2.py @@ -3,14 +3,14 @@ from __future__ import annotations import typing_extensions -from typing import List, Union, Optional +from typing import Union, Optional from datetime import date from typing_extensions import Literal, overload import httpx from .... import _legacy_response -from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven +from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, SequenceNotStr from ...._utils import required_args, maybe_transform, async_maybe_transform from .backtests import ( Backtests, @@ -74,7 +74,7 @@ def with_streaming_response(self) -> V2WithStreamingResponse: def create( self, *, - account_tokens: List[str], + account_tokens: SequenceNotStr[str], name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestAccountTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -118,7 +118,7 @@ def create( def create( self, *, - card_tokens: List[str], + card_tokens: SequenceNotStr[str], name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestCardTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -163,7 +163,7 @@ def create( self, *, program_level: bool, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestProgramLevelParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -209,14 +209,14 @@ def create( def create( self, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestAccountTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -282,7 +282,7 @@ def update( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -325,7 +325,7 @@ def update( self, auth_rule_token: str, *, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -368,7 +368,7 @@ def update( self, auth_rule_token: str, *, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, @@ -413,11 +413,11 @@ def update( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | 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. @@ -555,7 +555,7 @@ def apply( self, auth_rule_token: str, *, - account_tokens: List[str], + account_tokens: SequenceNotStr[str], # 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, @@ -588,7 +588,7 @@ def apply( self, auth_rule_token: str, *, - card_tokens: List[str], + card_tokens: SequenceNotStr[str], # 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, @@ -622,7 +622,7 @@ def apply( auth_rule_token: str, *, program_level: bool, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -657,10 +657,10 @@ def apply( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -936,7 +936,7 @@ def with_streaming_response(self) -> AsyncV2WithStreamingResponse: async def create( self, *, - account_tokens: List[str], + account_tokens: SequenceNotStr[str], name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestAccountTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -980,7 +980,7 @@ async def create( async def create( self, *, - card_tokens: List[str], + card_tokens: SequenceNotStr[str], name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestCardTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -1025,7 +1025,7 @@ async def create( self, *, program_level: bool, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestProgramLevelParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] @@ -1071,14 +1071,14 @@ async def create( async def create( self, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, parameters: v2_create_params.CreateAuthRuleRequestAccountTokensParameters | NotGiven = NOT_GIVEN, type: Literal["CONDITIONAL_BLOCK", "VELOCITY_LIMIT", "MERCHANT_LOCK", "CONDITIONAL_3DS_ACTION"] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -1144,7 +1144,7 @@ async def update( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1187,7 +1187,7 @@ async def update( self, auth_rule_token: str, *, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1230,7 +1230,7 @@ async def update( self, auth_rule_token: str, *, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, @@ -1275,11 +1275,11 @@ async def update( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, name: Optional[str] | NotGiven = NOT_GIVEN, state: Literal["INACTIVE"] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | 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. @@ -1417,7 +1417,7 @@ async def apply( self, auth_rule_token: str, *, - account_tokens: List[str], + account_tokens: SequenceNotStr[str], # 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, @@ -1450,7 +1450,7 @@ async def apply( self, auth_rule_token: str, *, - card_tokens: List[str], + card_tokens: SequenceNotStr[str], # 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, @@ -1484,7 +1484,7 @@ async def apply( auth_rule_token: str, *, program_level: bool, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -1519,10 +1519,10 @@ async def apply( self, auth_rule_token: str, *, - account_tokens: List[str] | NotGiven = NOT_GIVEN, - card_tokens: List[str] | NotGiven = NOT_GIVEN, + account_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, + card_tokens: SequenceNotStr[str] | NotGiven = NOT_GIVEN, program_level: bool | NotGiven = NOT_GIVEN, - excluded_card_tokens: List[str] | NotGiven = NOT_GIVEN, + excluded_card_tokens: SequenceNotStr[str] | 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, @@ -1791,7 +1791,7 @@ def __init__(self, v2: V2) -> None: ) self.apply = ( # pyright: ignore[reportDeprecated] _legacy_response.to_raw_response_wrapper( - v2.apply # pyright: ignore[reportDeprecated], + v2.apply, # pyright: ignore[reportDeprecated], ) ) self.draft = _legacy_response.to_raw_response_wrapper( @@ -1802,7 +1802,7 @@ def __init__(self, v2: V2) -> None: ) self.report = ( # pyright: ignore[reportDeprecated] _legacy_response.to_raw_response_wrapper( - v2.report # pyright: ignore[reportDeprecated], + v2.report, # pyright: ignore[reportDeprecated], ) ) self.retrieve_report = _legacy_response.to_raw_response_wrapper( @@ -1835,7 +1835,7 @@ def __init__(self, v2: AsyncV2) -> None: ) self.apply = ( # pyright: ignore[reportDeprecated] _legacy_response.async_to_raw_response_wrapper( - v2.apply # pyright: ignore[reportDeprecated], + v2.apply, # pyright: ignore[reportDeprecated], ) ) self.draft = _legacy_response.async_to_raw_response_wrapper( @@ -1846,7 +1846,7 @@ def __init__(self, v2: AsyncV2) -> None: ) self.report = ( # pyright: ignore[reportDeprecated] _legacy_response.async_to_raw_response_wrapper( - v2.report # pyright: ignore[reportDeprecated], + v2.report, # pyright: ignore[reportDeprecated], ) ) self.retrieve_report = _legacy_response.async_to_raw_response_wrapper( @@ -1879,7 +1879,7 @@ def __init__(self, v2: V2) -> None: ) self.apply = ( # pyright: ignore[reportDeprecated] to_streamed_response_wrapper( - v2.apply # pyright: ignore[reportDeprecated], + v2.apply, # pyright: ignore[reportDeprecated], ) ) self.draft = to_streamed_response_wrapper( @@ -1890,7 +1890,7 @@ def __init__(self, v2: V2) -> None: ) self.report = ( # pyright: ignore[reportDeprecated] to_streamed_response_wrapper( - v2.report # pyright: ignore[reportDeprecated], + v2.report, # pyright: ignore[reportDeprecated], ) ) self.retrieve_report = to_streamed_response_wrapper( @@ -1923,7 +1923,7 @@ def __init__(self, v2: AsyncV2) -> None: ) self.apply = ( # pyright: ignore[reportDeprecated] async_to_streamed_response_wrapper( - v2.apply # pyright: ignore[reportDeprecated], + v2.apply, # pyright: ignore[reportDeprecated], ) ) self.draft = async_to_streamed_response_wrapper( @@ -1934,7 +1934,7 @@ def __init__(self, v2: AsyncV2) -> None: ) self.report = ( # pyright: ignore[reportDeprecated] async_to_streamed_response_wrapper( - v2.report # pyright: ignore[reportDeprecated], + v2.report, # pyright: ignore[reportDeprecated], ) ) self.retrieve_report = async_to_streamed_response_wrapper( diff --git a/src/lithic/resources/book_transfers.py b/src/lithic/resources/book_transfers.py index 32e90b42..b9dd9fa7 100644 --- a/src/lithic/resources/book_transfers.py +++ b/src/lithic/resources/book_transfers.py @@ -84,6 +84,7 @@ def create( "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ], token: str | NotGiven = NOT_GIVEN, external_id: str | NotGiven = NOT_GIVEN, @@ -379,6 +380,7 @@ async def create( "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ], token: str | NotGiven = NOT_GIVEN, external_id: str | NotGiven = NOT_GIVEN, diff --git a/src/lithic/resources/disputes.py b/src/lithic/resources/disputes.py index d29e9ab8..1e8cec5e 100644 --- a/src/lithic/resources/disputes.py +++ b/src/lithic/resources/disputes.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from datetime import datetime from typing_extensions import Literal @@ -16,7 +16,7 @@ dispute_list_evidences_params, dispute_initiate_evidence_upload_params, ) -from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes +from .._types import NOT_GIVEN, Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, SequenceNotStr from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -242,7 +242,7 @@ def list( "SUBMITTED", ] | NotGiven = NOT_GIVEN, - transaction_tokens: List[str] | NotGiven = NOT_GIVEN, + transaction_tokens: SequenceNotStr[str] | 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, @@ -758,7 +758,7 @@ def list( "SUBMITTED", ] | NotGiven = NOT_GIVEN, - transaction_tokens: List[str] | NotGiven = NOT_GIVEN, + transaction_tokens: SequenceNotStr[str] | 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, diff --git a/src/lithic/resources/external_bank_accounts/external_bank_accounts.py b/src/lithic/resources/external_bank_accounts/external_bank_accounts.py index 4805c971..9613c792 100644 --- a/src/lithic/resources/external_bank_accounts/external_bank_accounts.py +++ b/src/lithic/resources/external_bank_accounts/external_bank_accounts.py @@ -18,7 +18,7 @@ external_bank_account_retry_prenote_params, external_bank_account_retry_micro_deposits_params, ) -from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr from ..._utils import required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -547,7 +547,7 @@ def list( *, account_token: str | NotGiven = NOT_GIVEN, account_types: List[Literal["CHECKING", "SAVINGS"]] | NotGiven = NOT_GIVEN, - countries: List[str] | NotGiven = NOT_GIVEN, + countries: SequenceNotStr[str] | NotGiven = NOT_GIVEN, ending_before: str | NotGiven = NOT_GIVEN, owner_types: List[OwnerType] | NotGiven = NOT_GIVEN, page_size: int | NotGiven = NOT_GIVEN, @@ -1190,7 +1190,7 @@ def list( *, account_token: str | NotGiven = NOT_GIVEN, account_types: List[Literal["CHECKING", "SAVINGS"]] | NotGiven = NOT_GIVEN, - countries: List[str] | NotGiven = NOT_GIVEN, + countries: SequenceNotStr[str] | NotGiven = NOT_GIVEN, ending_before: str | NotGiven = NOT_GIVEN, owner_types: List[OwnerType] | NotGiven = NOT_GIVEN, page_size: int | NotGiven = NOT_GIVEN, diff --git a/src/lithic/resources/reports/settlement/settlement.py b/src/lithic/resources/reports/settlement/settlement.py index fee2eff8..184f8d51 100644 --- a/src/lithic/resources/reports/settlement/settlement.py +++ b/src/lithic/resources/reports/settlement/settlement.py @@ -75,7 +75,7 @@ def list_details( ending_before: A cursor representing an item's token before which a page of results should end. Used to retrieve the previous page of results before this item. - page_size: Page size (for pagination). + page_size: Number of records per page. starting_after: A cursor representing an item's token after which a page of results should begin. Used to retrieve the next page of results after this item. @@ -190,7 +190,7 @@ def list_details( ending_before: A cursor representing an item's token before which a page of results should end. Used to retrieve the previous page of results before this item. - page_size: Page size (for pagination). + page_size: Number of records per page. starting_after: A cursor representing an item's token after which a page of results should begin. Used to retrieve the next page of results after this item. diff --git a/src/lithic/types/__init__.py b/src/lithic/types/__init__.py index 295b2fe5..0cff09a2 100644 --- a/src/lithic/types/__init__.py +++ b/src/lithic/types/__init__.py @@ -47,6 +47,7 @@ from .card_create_params import CardCreateParams as CardCreateParams from .card_update_params import CardUpdateParams as CardUpdateParams from .event_subscription import EventSubscription as EventSubscription +from .wire_party_details import WirePartyDetails as WirePartyDetails from .account_list_params import AccountListParams as AccountListParams from .balance_list_params import BalanceListParams as BalanceListParams from .card_embed_response import CardEmbedResponse as CardEmbedResponse diff --git a/src/lithic/types/account_activity_list_response.py b/src/lithic/types/account_activity_list_response.py index 0abcca44..1bf9c440 100644 --- a/src/lithic/types/account_activity_list_response.py +++ b/src/lithic/types/account_activity_list_response.py @@ -8,6 +8,7 @@ from .transaction import Transaction from .external_payment import ExternalPayment from .external_resource import ExternalResource +from .wire_party_details import WirePartyDetails from .management_operation_transaction import ManagementOperationTransaction __all__ = [ @@ -235,6 +236,7 @@ class BookTransferTransactionEvent(BaseModel): "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ] """Type of the book transfer""" @@ -416,38 +418,24 @@ class PaymentTransactionMethodAttributesACHMethodAttributes(BaseModel): class PaymentTransactionMethodAttributesWireMethodAttributes(BaseModel): - wire_transfer_type: Literal["FEDWIRE", "SWIFT"] + wire_network: Literal["FEDWIRE", "SWIFT"] """Type of wire transfer""" - external_bank_name: Optional[str] = None - """External bank name""" + creditor: Optional[WirePartyDetails] = None - external_bank_routing_number: Optional[str] = None - """External bank routing number""" + debtor: Optional[WirePartyDetails] = None - external_individual_name: Optional[str] = None - """External individual name""" - - imad: Optional[str] = None - """IMAD""" - - lithic_bank_name: Optional[str] = None - """Lithic bank name""" - - lithic_bank_routing_number: Optional[str] = None - """Lithic bank routing number""" - - lithic_individual_name: Optional[str] = None - """Lithic individual name""" - - omad: Optional[str] = None - """OMAD""" + message_id: Optional[str] = None + """ + Point to point reference identifier, as assigned by the instructing party, used + for tracking the message through the Fedwire system + """ - previous_transfer: Optional[str] = None - """UUID of previous transfer if this is a retry""" + remittance_information: Optional[str] = None + """Payment details or invoice reference""" - wire_token: Optional[str] = None - """Wire token""" + wire_message_type: Optional[str] = None + """Type of wire message""" PaymentTransactionMethodAttributes: TypeAlias = Union[ @@ -537,6 +525,19 @@ class PaymentTransaction(BaseModel): external_bank_account_token: Optional[str] = None """External bank account token""" + type: Optional[ + Literal[ + "ORIGINATION_CREDIT", + "ORIGINATION_DEBIT", + "RECEIPT_CREDIT", + "RECEIPT_DEBIT", + "WIRE_INBOUND_PAYMENT", + "WIRE_INBOUND_ADMIN", + "WIRE_OUTBOUND_PAYMENT", + "WIRE_OUTBOUND_ADMIN", + ] + ] = None + user_defined_id: Optional[str] = None """User-defined identifier""" diff --git a/src/lithic/types/account_activity_retrieve_transaction_response.py b/src/lithic/types/account_activity_retrieve_transaction_response.py index e26f2443..97063fbb 100644 --- a/src/lithic/types/account_activity_retrieve_transaction_response.py +++ b/src/lithic/types/account_activity_retrieve_transaction_response.py @@ -8,6 +8,7 @@ from .transaction import Transaction from .external_payment import ExternalPayment from .external_resource import ExternalResource +from .wire_party_details import WirePartyDetails from .management_operation_transaction import ManagementOperationTransaction __all__ = [ @@ -235,6 +236,7 @@ class BookTransferTransactionEvent(BaseModel): "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ] """Type of the book transfer""" @@ -416,38 +418,24 @@ class PaymentTransactionMethodAttributesACHMethodAttributes(BaseModel): class PaymentTransactionMethodAttributesWireMethodAttributes(BaseModel): - wire_transfer_type: Literal["FEDWIRE", "SWIFT"] + wire_network: Literal["FEDWIRE", "SWIFT"] """Type of wire transfer""" - external_bank_name: Optional[str] = None - """External bank name""" + creditor: Optional[WirePartyDetails] = None - external_bank_routing_number: Optional[str] = None - """External bank routing number""" + debtor: Optional[WirePartyDetails] = None - external_individual_name: Optional[str] = None - """External individual name""" - - imad: Optional[str] = None - """IMAD""" - - lithic_bank_name: Optional[str] = None - """Lithic bank name""" - - lithic_bank_routing_number: Optional[str] = None - """Lithic bank routing number""" - - lithic_individual_name: Optional[str] = None - """Lithic individual name""" - - omad: Optional[str] = None - """OMAD""" + message_id: Optional[str] = None + """ + Point to point reference identifier, as assigned by the instructing party, used + for tracking the message through the Fedwire system + """ - previous_transfer: Optional[str] = None - """UUID of previous transfer if this is a retry""" + remittance_information: Optional[str] = None + """Payment details or invoice reference""" - wire_token: Optional[str] = None - """Wire token""" + wire_message_type: Optional[str] = None + """Type of wire message""" PaymentTransactionMethodAttributes: TypeAlias = Union[ @@ -537,6 +525,19 @@ class PaymentTransaction(BaseModel): external_bank_account_token: Optional[str] = None """External bank account token""" + type: Optional[ + Literal[ + "ORIGINATION_CREDIT", + "ORIGINATION_DEBIT", + "RECEIPT_CREDIT", + "RECEIPT_DEBIT", + "WIRE_INBOUND_PAYMENT", + "WIRE_INBOUND_ADMIN", + "WIRE_OUTBOUND_PAYMENT", + "WIRE_OUTBOUND_ADMIN", + ] + ] = None + user_defined_id: Optional[str] = None """User-defined identifier""" diff --git a/src/lithic/types/account_holder_create_params.py b/src/lithic/types/account_holder_create_params.py index bfa6d1b3..d450f1e8 100644 --- a/src/lithic/types/account_holder_create_params.py +++ b/src/lithic/types/account_holder_create_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Literal, Required, TypeAlias, TypedDict +from .._types import SequenceNotStr from .shared_params.address import Address __all__ = [ @@ -14,6 +15,10 @@ "KYBBusinessEntity", "KYBControlPerson", "KYBBeneficialOwnerEntity", + "KYBDelegated", + "KYBDelegatedBusinessEntity", + "KYBDelegatedBeneficialOwnerIndividual", + "KYBDelegatedControlPerson", "KYC", "KYCIndividual", "KYCExempt", @@ -137,7 +142,7 @@ class KYBBusinessEntity(TypedDict, total=False): legal_business_name: Required[str] """Legal (formal) business name.""" - phone_numbers: Required[List[str]] + phone_numbers: Required[SequenceNotStr[str]] """ One or more of the business's phone number(s), entered as a list in E.164 format. @@ -204,7 +209,7 @@ class KYBBeneficialOwnerEntity(TypedDict, total=False): legal_business_name: Required[str] """Legal (formal) business name.""" - phone_numbers: Required[List[str]] + phone_numbers: Required[SequenceNotStr[str]] """ One or more of the business's phone number(s), entered as a list in E.164 format. @@ -220,6 +225,159 @@ class KYBBeneficialOwnerEntity(TypedDict, total=False): """Parent company name (if applicable).""" +class KYBDelegated(TypedDict, total=False): + business_entity: Required[KYBDelegatedBusinessEntity] + """Information for business for which the account is being opened.""" + + beneficial_owner_individuals: Iterable[KYBDelegatedBeneficialOwnerIndividual] + """ + You can submit a list of all direct and indirect individuals with 25% or more + ownership in the company. A maximum of 4 beneficial owners can be submitted. If + no individual owns 25% of the company you do not need to send beneficial owner + information. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section I) for more background on individuals that should be included. + """ + + control_person: KYBDelegatedControlPerson + """ + An individual with significant responsibility for managing the legal entity + (e.g., a Chief Executive Officer, Chief Financial Officer, Chief Operating + Officer, Managing Member, General Partner, President, Vice President, or + Treasurer). This can be an executive, or someone who will have program-wide + access to the cards that Lithic will provide. In some cases, this individual + could also be a beneficial owner listed above. See + [FinCEN requirements](https://www.fincen.gov/sites/default/files/shared/CDD_Rev6.7_Sept_2017_Certificate.pdf) + (Section II) for more background. + """ + + external_id: str + """ + A user provided id that can be used to link an account holder with an external + system + """ + + nature_of_business: str + """ + Short description of the company's line of business (i.e., what does the company + do?). + """ + + tos_timestamp: str + """ + An RFC 3339 timestamp indicating when the account holder accepted the applicable + legal agreements (e.g., cardholder terms) as agreed upon during API customer's + implementation with Lithic. + """ + + website_url: str + """Company website URL.""" + + workflow: Literal["KYB_DELEGATED"] + """Specifies the type of KYB workflow to run.""" + + +class KYBDelegatedBusinessEntity(TypedDict, total=False): + address: Required[Address] + """ + Business's physical address - PO boxes, UPS drops, and FedEx drops are not + acceptable; APO/FPO are acceptable. + """ + + legal_business_name: Required[str] + """Legal (formal) business name.""" + + dba_business_name: str + """ + Any name that the business operates under that is not its legal business name + (if applicable). + """ + + government_id: str + """Government-issued identification number. + + US Federal Employer Identification Numbers (EIN) are currently supported, + entered as full nine-digits, with or without hyphens. + """ + + parent_company: str + """Parent company name (if applicable).""" + + phone_numbers: SequenceNotStr[str] + """ + One or more of the business's phone number(s), entered as a list in E.164 + format. + """ + + +class KYBDelegatedBeneficialOwnerIndividual(TypedDict, total=False): + address: Required[Address] + """ + Individual's current address - PO boxes, UPS drops, and FedEx drops are not + acceptable; APO/FPO are acceptable. Only USA addresses are currently supported. + """ + + dob: Required[str] + """Individual's date of birth, as an RFC 3339 date.""" + + email: Required[str] + """ + Individual's email address. If utilizing Lithic for chargeback processing, this + customer email address may be used to communicate dispute status and resolution. + """ + + first_name: Required[str] + """Individual's first name, as it appears on government-issued identity documents.""" + + government_id: Required[str] + """ + Government-issued identification number (required for identity verification and + compliance with banking regulations). Social Security Numbers (SSN) and + Individual Taxpayer Identification Numbers (ITIN) are currently supported, + entered as full nine-digits, with or without hyphens + """ + + last_name: Required[str] + """Individual's last name, as it appears on government-issued identity documents.""" + + phone_number: str + """Individual's phone number, entered in E.164 format.""" + + +class KYBDelegatedControlPerson(TypedDict, total=False): + address: Required[Address] + """ + Individual's current address - PO boxes, UPS drops, and FedEx drops are not + acceptable; APO/FPO are acceptable. Only USA addresses are currently supported. + """ + + dob: Required[str] + """Individual's date of birth, as an RFC 3339 date.""" + + email: Required[str] + """ + Individual's email address. If utilizing Lithic for chargeback processing, this + customer email address may be used to communicate dispute status and resolution. + """ + + first_name: Required[str] + """Individual's first name, as it appears on government-issued identity documents.""" + + government_id: Required[str] + """ + Government-issued identification number (required for identity verification and + compliance with banking regulations). Social Security Numbers (SSN) and + Individual Taxpayer Identification Numbers (ITIN) are currently supported, + entered as full nine-digits, with or without hyphens + """ + + last_name: Required[str] + """Individual's last name, as it appears on government-issued identity documents.""" + + phone_number: str + """Individual's phone number, entered in E.164 format.""" + + class KYC(TypedDict, total=False): individual: Required[KYCIndividual] """ @@ -325,4 +483,4 @@ class KYCExempt(TypedDict, total=False): """ -AccountHolderCreateParams: TypeAlias = Union[KYB, KYC, KYCExempt] +AccountHolderCreateParams: TypeAlias = Union[KYB, KYBDelegated, KYC, KYCExempt] diff --git a/src/lithic/types/account_holder_simulate_enrollment_document_review_params.py b/src/lithic/types/account_holder_simulate_enrollment_document_review_params.py index 3843d08f..8ad2d225 100644 --- a/src/lithic/types/account_holder_simulate_enrollment_document_review_params.py +++ b/src/lithic/types/account_holder_simulate_enrollment_document_review_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr + __all__ = ["AccountHolderSimulateEnrollmentDocumentReviewParams"] @@ -15,7 +16,7 @@ class AccountHolderSimulateEnrollmentDocumentReviewParams(TypedDict, total=False status: Required[Literal["UPLOADED", "ACCEPTED", "REJECTED", "PARTIAL_APPROVAL"]] """An account holder document's upload status for use within the simulation.""" - accepted_entity_status_reasons: List[str] + accepted_entity_status_reasons: SequenceNotStr[str] """A list of status reasons associated with a KYB account holder in PENDING_REVIEW""" status_reason: Literal[ diff --git a/src/lithic/types/account_holder_update_params.py b/src/lithic/types/account_holder_update_params.py index 89b2caf5..4c92d81d 100644 --- a/src/lithic/types/account_holder_update_params.py +++ b/src/lithic/types/account_holder_update_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Required, TypeAlias, TypedDict +from .._types import SequenceNotStr from .address_update_param import AddressUpdateParam __all__ = [ @@ -97,7 +98,7 @@ class KYBPatchRequestBeneficialOwnerEntity(TypedDict, total=False): parent_company: str """Parent company name (if applicable).""" - phone_numbers: List[str] + phone_numbers: SequenceNotStr[str] """ One or more of the business's phone number(s), entered as a list in E.164 format. @@ -171,7 +172,7 @@ class KYBPatchRequestBusinessEntity(TypedDict, total=False): parent_company: str """Parent company name (if applicable).""" - phone_numbers: List[str] + phone_numbers: SequenceNotStr[str] """ One or more of the business's phone number(s), entered as a list in E.164 format. diff --git a/src/lithic/types/auth_rules/auth_rule_condition.py b/src/lithic/types/auth_rules/auth_rule_condition.py index 7de1a850..4343710e 100644 --- a/src/lithic/types/auth_rules/auth_rule_condition.py +++ b/src/lithic/types/auth_rules/auth_rule_condition.py @@ -61,7 +61,18 @@ class AuthRuleCondition(BaseModel): """ operation: Optional[ - Literal["IS_ONE_OF", "IS_NOT_ONE_OF", "MATCHES", "DOES_NOT_MATCH", "IS_GREATER_THAN", "IS_LESS_THAN"] + Literal[ + "IS_ONE_OF", + "IS_NOT_ONE_OF", + "MATCHES", + "DOES_NOT_MATCH", + "IS_EQUAL_TO", + "IS_NOT_EQUAL_TO", + "IS_GREATER_THAN", + "IS_GREATER_THAN_OR_EQUAL_TO", + "IS_LESS_THAN", + "IS_LESS_THAN_OR_EQUAL_TO", + ] ] = None """The operation to apply to the attribute""" diff --git a/src/lithic/types/auth_rules/auth_rule_condition_param.py b/src/lithic/types/auth_rules/auth_rule_condition_param.py index 960d4e71..b0bf86d3 100644 --- a/src/lithic/types/auth_rules/auth_rule_condition_param.py +++ b/src/lithic/types/auth_rules/auth_rule_condition_param.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Literal, TypedDict +from ..._types import SequenceNotStr from .conditional_attribute import ConditionalAttribute __all__ = ["AuthRuleConditionParam"] @@ -61,8 +62,19 @@ class AuthRuleConditionParam(TypedDict, total=False): `SAMSUNG_PAY`, `MASTERPASS`, `MERCHANT`, `OTHER`, `NONE`. """ - operation: Literal["IS_ONE_OF", "IS_NOT_ONE_OF", "MATCHES", "DOES_NOT_MATCH", "IS_GREATER_THAN", "IS_LESS_THAN"] + operation: Literal[ + "IS_ONE_OF", + "IS_NOT_ONE_OF", + "MATCHES", + "DOES_NOT_MATCH", + "IS_EQUAL_TO", + "IS_NOT_EQUAL_TO", + "IS_GREATER_THAN", + "IS_GREATER_THAN_OR_EQUAL_TO", + "IS_LESS_THAN", + "IS_LESS_THAN_OR_EQUAL_TO", + ] """The operation to apply to the attribute""" - value: Union[str, int, List[str]] + value: Union[str, int, SequenceNotStr[str]] """A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`""" diff --git a/src/lithic/types/auth_rules/conditional_3ds_action_parameters.py b/src/lithic/types/auth_rules/conditional_3ds_action_parameters.py index 307d3f4d..1db7b1c1 100644 --- a/src/lithic/types/auth_rules/conditional_3ds_action_parameters.py +++ b/src/lithic/types/auth_rules/conditional_3ds_action_parameters.py @@ -45,7 +45,18 @@ class Condition(BaseModel): """ operation: Optional[ - Literal["IS_ONE_OF", "IS_NOT_ONE_OF", "MATCHES", "DOES_NOT_MATCH", "IS_GREATER_THAN", "IS_LESS_THAN"] + Literal[ + "IS_ONE_OF", + "IS_NOT_ONE_OF", + "MATCHES", + "DOES_NOT_MATCH", + "IS_EQUAL_TO", + "IS_NOT_EQUAL_TO", + "IS_GREATER_THAN", + "IS_GREATER_THAN_OR_EQUAL_TO", + "IS_LESS_THAN", + "IS_LESS_THAN_OR_EQUAL_TO", + ] ] = None """The operation to apply to the attribute""" diff --git a/src/lithic/types/auth_rules/conditional_3ds_action_parameters_param.py b/src/lithic/types/auth_rules/conditional_3ds_action_parameters_param.py index 29d5e084..43c4759b 100644 --- a/src/lithic/types/auth_rules/conditional_3ds_action_parameters_param.py +++ b/src/lithic/types/auth_rules/conditional_3ds_action_parameters_param.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Union, Iterable from typing_extensions import Literal, Required, TypedDict +from ..._types import SequenceNotStr + __all__ = ["Conditional3DSActionParametersParam", "Condition"] @@ -42,10 +44,21 @@ class Condition(TypedDict, total=False): - `MESSAGE_CATEGORY`: The category of the authentication being processed. """ - operation: Literal["IS_ONE_OF", "IS_NOT_ONE_OF", "MATCHES", "DOES_NOT_MATCH", "IS_GREATER_THAN", "IS_LESS_THAN"] + operation: Literal[ + "IS_ONE_OF", + "IS_NOT_ONE_OF", + "MATCHES", + "DOES_NOT_MATCH", + "IS_EQUAL_TO", + "IS_NOT_EQUAL_TO", + "IS_GREATER_THAN", + "IS_GREATER_THAN_OR_EQUAL_TO", + "IS_LESS_THAN", + "IS_LESS_THAN_OR_EQUAL_TO", + ] """The operation to apply to the attribute""" - value: Union[str, int, List[str]] + value: Union[str, int, SequenceNotStr[str]] """A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`""" diff --git a/src/lithic/types/auth_rules/v2_apply_params.py b/src/lithic/types/auth_rules/v2_apply_params.py index e44e79e7..7e419964 100644 --- a/src/lithic/types/auth_rules/v2_apply_params.py +++ b/src/lithic/types/auth_rules/v2_apply_params.py @@ -2,9 +2,11 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from typing_extensions import Required, TypeAlias, TypedDict +from ..._types import SequenceNotStr + __all__ = [ "V2ApplyParams", "ApplyAuthRuleRequestAccountTokens", @@ -14,12 +16,12 @@ class ApplyAuthRuleRequestAccountTokens(TypedDict, total=False): - account_tokens: Required[List[str]] + account_tokens: Required[SequenceNotStr[str]] """Account tokens to which the Auth Rule applies.""" class ApplyAuthRuleRequestCardTokens(TypedDict, total=False): - card_tokens: Required[List[str]] + card_tokens: Required[SequenceNotStr[str]] """Card tokens to which the Auth Rule applies.""" @@ -27,7 +29,7 @@ class ApplyAuthRuleRequestProgramLevel(TypedDict, total=False): program_level: Required[bool] """Whether the Auth Rule applies to all authorizations on the card program.""" - excluded_card_tokens: List[str] + excluded_card_tokens: SequenceNotStr[str] """Card tokens to which the Auth Rule does not apply.""" diff --git a/src/lithic/types/auth_rules/v2_create_params.py b/src/lithic/types/auth_rules/v2_create_params.py index 283ea9ed..5a937a3a 100644 --- a/src/lithic/types/auth_rules/v2_create_params.py +++ b/src/lithic/types/auth_rules/v2_create_params.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union, Optional +from typing import Union, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict +from ..._types import SequenceNotStr from .velocity_limit_params_param import VelocityLimitParamsParam from .merchant_lock_parameters_param import MerchantLockParametersParam from .conditional_block_parameters_param import ConditionalBlockParametersParam @@ -22,7 +23,7 @@ class CreateAuthRuleRequestAccountTokens(TypedDict, total=False): - account_tokens: Required[List[str]] + account_tokens: Required[SequenceNotStr[str]] """Account tokens to which the Auth Rule applies.""" name: Optional[str] @@ -52,7 +53,7 @@ class CreateAuthRuleRequestAccountTokens(TypedDict, total=False): class CreateAuthRuleRequestCardTokens(TypedDict, total=False): - card_tokens: Required[List[str]] + card_tokens: Required[SequenceNotStr[str]] """Card tokens to which the Auth Rule applies.""" name: Optional[str] @@ -85,7 +86,7 @@ class CreateAuthRuleRequestProgramLevel(TypedDict, total=False): program_level: Required[bool] """Whether the Auth Rule applies to all authorizations on the card program.""" - excluded_card_tokens: List[str] + excluded_card_tokens: SequenceNotStr[str] """Card tokens to which the Auth Rule does not apply.""" name: Optional[str] diff --git a/src/lithic/types/auth_rules/v2_update_params.py b/src/lithic/types/auth_rules/v2_update_params.py index 042cf558..306a4236 100644 --- a/src/lithic/types/auth_rules/v2_update_params.py +++ b/src/lithic/types/auth_rules/v2_update_params.py @@ -2,14 +2,16 @@ from __future__ import annotations -from typing import List, Union, Optional +from typing import Union, Optional from typing_extensions import Literal, TypeAlias, TypedDict +from ..._types import SequenceNotStr + __all__ = ["V2UpdateParams", "AccountLevelRule", "CardLevelRule", "ProgramLevelRule"] class AccountLevelRule(TypedDict, total=False): - account_tokens: List[str] + account_tokens: SequenceNotStr[str] """Account tokens to which the Auth Rule applies.""" name: Optional[str] @@ -25,7 +27,7 @@ class AccountLevelRule(TypedDict, total=False): class CardLevelRule(TypedDict, total=False): - card_tokens: List[str] + card_tokens: SequenceNotStr[str] """Card tokens to which the Auth Rule applies.""" name: Optional[str] @@ -41,7 +43,7 @@ class CardLevelRule(TypedDict, total=False): class ProgramLevelRule(TypedDict, total=False): - excluded_card_tokens: List[str] + excluded_card_tokens: SequenceNotStr[str] """Card tokens to which the Auth Rule does not apply.""" name: Optional[str] diff --git a/src/lithic/types/auth_rules/velocity_limit_params_param.py b/src/lithic/types/auth_rules/velocity_limit_params_param.py index c57926ea..4faee40f 100644 --- a/src/lithic/types/auth_rules/velocity_limit_params_param.py +++ b/src/lithic/types/auth_rules/velocity_limit_params_param.py @@ -2,36 +2,37 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from typing_extensions import Literal, Required, TypedDict +from ..._types import SequenceNotStr from .velocity_limit_params_period_window_param import VelocityLimitParamsPeriodWindowParam __all__ = ["VelocityLimitParamsParam", "Filters"] class Filters(TypedDict, total=False): - exclude_countries: Optional[List[str]] + exclude_countries: Optional[SequenceNotStr[str]] """ISO-3166-1 alpha-3 Country Codes to exclude from the velocity calculation. Transactions matching any of the provided will be excluded from the calculated velocity. """ - exclude_mccs: Optional[List[str]] + exclude_mccs: Optional[SequenceNotStr[str]] """Merchant Category Codes to exclude from the velocity calculation. Transactions matching this MCC will be excluded from the calculated velocity. """ - include_countries: Optional[List[str]] + include_countries: Optional[SequenceNotStr[str]] """ISO-3166-1 alpha-3 Country Codes to include in the velocity calculation. Transactions not matching any of the provided will not be included in the calculated velocity. """ - include_mccs: Optional[List[str]] + include_mccs: Optional[SequenceNotStr[str]] """Merchant Category Codes to include in the velocity calculation. Transactions not matching this MCC will not be included in the calculated diff --git a/src/lithic/types/book_transfer_create_params.py b/src/lithic/types/book_transfer_create_params.py index 54b82744..8346efc1 100644 --- a/src/lithic/types/book_transfer_create_params.py +++ b/src/lithic/types/book_transfer_create_params.py @@ -69,6 +69,7 @@ class BookTransferCreateParams(TypedDict, total=False): "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ] ] """Type of the book transfer""" diff --git a/src/lithic/types/book_transfer_response.py b/src/lithic/types/book_transfer_response.py index b58bd978..de4fd9d2 100644 --- a/src/lithic/types/book_transfer_response.py +++ b/src/lithic/types/book_transfer_response.py @@ -71,6 +71,7 @@ class Event(BaseModel): "DISPUTE_WON", "SERVICE", "TRANSFER", + "COLLECTION", ] """Type of the book transfer""" diff --git a/src/lithic/types/dispute_list_params.py b/src/lithic/types/dispute_list_params.py index fd12fdf5..f83e42ab 100644 --- a/src/lithic/types/dispute_list_params.py +++ b/src/lithic/types/dispute_list_params.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import List, Union +from typing import Union from datetime import datetime from typing_extensions import Literal, Annotated, TypedDict +from .._types import SequenceNotStr from .._utils import PropertyInfo __all__ = ["DisputeListParams"] @@ -52,5 +53,5 @@ class DisputeListParams(TypedDict, total=False): ] """List disputes of a specific status.""" - transaction_tokens: List[str] + transaction_tokens: SequenceNotStr[str] """Transaction tokens to filter by.""" diff --git a/src/lithic/types/external_bank_account_list_params.py b/src/lithic/types/external_bank_account_list_params.py index 4a0a267d..3d703893 100644 --- a/src/lithic/types/external_bank_account_list_params.py +++ b/src/lithic/types/external_bank_account_list_params.py @@ -5,6 +5,7 @@ from typing import List from typing_extensions import Literal, TypedDict +from .._types import SequenceNotStr from .owner_type import OwnerType __all__ = ["ExternalBankAccountListParams"] @@ -15,7 +16,7 @@ class ExternalBankAccountListParams(TypedDict, total=False): account_types: List[Literal["CHECKING", "SAVINGS"]] - countries: List[str] + countries: SequenceNotStr[str] ending_before: str """A cursor representing an item's token before which a page of results should end. diff --git a/src/lithic/types/financial_account.py b/src/lithic/types/financial_account.py index e554064b..b3fa2bc8 100644 --- a/src/lithic/types/financial_account.py +++ b/src/lithic/types/financial_account.py @@ -57,7 +57,7 @@ class FinancialAccount(BaseModel): "SECURITY", "PROGRAM_RECEIVABLES", "COLLECTION", - "BANK_ACCOUNTS_PAYABLE", + "PROGRAM_BANK_ACCOUNTS_PAYABLE", ] updated: datetime diff --git a/src/lithic/types/kyb_param.py b/src/lithic/types/kyb_param.py index 78e04e29..9977f636 100644 --- a/src/lithic/types/kyb_param.py +++ b/src/lithic/types/kyb_param.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Iterable +from typing import Iterable from typing_extensions import Literal, Required, TypedDict +from .._types import SequenceNotStr from .shared_params.address import Address __all__ = ["KYBParam", "BeneficialOwnerIndividual", "BusinessEntity", "ControlPerson", "BeneficialOwnerEntity"] @@ -61,7 +62,7 @@ class BusinessEntity(TypedDict, total=False): legal_business_name: Required[str] """Legal (formal) business name.""" - phone_numbers: Required[List[str]] + phone_numbers: Required[SequenceNotStr[str]] """ One or more of the business's phone number(s), entered as a list in E.164 format. @@ -128,7 +129,7 @@ class BeneficialOwnerEntity(TypedDict, total=False): legal_business_name: Required[str] """Legal (formal) business name.""" - phone_numbers: Required[List[str]] + phone_numbers: Required[SequenceNotStr[str]] """ One or more of the business's phone number(s), entered as a list in E.164 format. diff --git a/src/lithic/types/payment.py b/src/lithic/types/payment.py index a529d742..afd06438 100644 --- a/src/lithic/types/payment.py +++ b/src/lithic/types/payment.py @@ -181,12 +181,10 @@ class Payment(BaseModel): "ORIGINATION_DEBIT", "RECEIPT_CREDIT", "RECEIPT_DEBIT", - "CUSTOMER_TRANSFER", - "DRAWDOWN_PAYMENT", - "REVERSAL_PAYMENT", - "DRAWDOWN_REQUEST", - "REVERSAL_REQUEST", - "DRAWDOWN_REFUSAL", + "WIRE_INBOUND_PAYMENT", + "WIRE_INBOUND_ADMIN", + "WIRE_OUTBOUND_PAYMENT", + "WIRE_OUTBOUND_ADMIN", ] ] = None """Payment type indicating the specific ACH message or Fedwire transfer type""" diff --git a/src/lithic/types/reports/settlement_list_details_params.py b/src/lithic/types/reports/settlement_list_details_params.py index 003f65d0..6dc81d39 100644 --- a/src/lithic/types/reports/settlement_list_details_params.py +++ b/src/lithic/types/reports/settlement_list_details_params.py @@ -15,7 +15,7 @@ class SettlementListDetailsParams(TypedDict, total=False): """ page_size: int - """Page size (for pagination).""" + """Number of records per page.""" starting_after: str """A cursor representing an item's token after which a page of results should diff --git a/src/lithic/types/shared/instance_financial_account_type.py b/src/lithic/types/shared/instance_financial_account_type.py index 806369f8..72a4a348 100644 --- a/src/lithic/types/shared/instance_financial_account_type.py +++ b/src/lithic/types/shared/instance_financial_account_type.py @@ -14,5 +14,5 @@ "SECURITY", "PROGRAM_RECEIVABLES", "COLLECTION", - "BANK_ACCOUNTS_PAYABLE", + "PROGRAM_BANK_ACCOUNTS_PAYABLE", ] diff --git a/src/lithic/types/three_ds/authentication_retrieve_response.py b/src/lithic/types/three_ds/authentication_retrieve_response.py index d202af6c..95ae8a51 100644 --- a/src/lithic/types/three_ds/authentication_retrieve_response.py +++ b/src/lithic/types/three_ds/authentication_retrieve_response.py @@ -232,17 +232,41 @@ class AdditionalData(BaseModel): class App(BaseModel): + device: Optional[str] = None + """Device model: e.g. "Apple iPhone 16".""" + device_info: Optional[str] = None - """ - Device information gathered from the cardholder's device - JSON name/value pairs - that is Base64url encoded. Maps to EMV 3DS field `deviceInfo`. + """Raw device information - base64-encoded JSON object. + + Maps to EMV 3DS field `deviceInfo`. """ ip: Optional[str] = None - """External IP address used by the app generating the 3DS authentication request. + """IP address of the device.""" - Maps to EMV 3DS field `appIp`. - """ + latitude: Optional[float] = None + """Latitude coordinate of current device location.""" + + locale: Optional[str] = None + """Device locale: e.g. "en-US".""" + + longitude: Optional[float] = None + """Longitude coordinate of current device location.""" + + os: Optional[str] = None + """Operating System: e.g. "Android 12", "iOS 17.1".""" + + platform: Optional[str] = None + """Device platform: Android, iOS, Windows, etc.""" + + screen_height: Optional[int] = None + """Screen height in pixels.""" + + screen_width: Optional[int] = None + """Screen width in pixels.""" + + time_zone: Optional[str] = None + """Time zone offset in minutes between UTC and device local time.""" class Browser(BaseModel): @@ -278,10 +302,9 @@ class Browser(BaseModel): """ time_zone: Optional[str] = None - """ - Time zone of the cardholder's browser offset in minutes between UTC and the - cardholder browser's local time. The offset is positive if the local time is - behind UTC and negative if it is ahead. Maps to EMV 3DS field `browserTz`. + """Time zone offset in minutes between UTC and browser local time. + + Maps to EMV 3DS field `browserTz`. """ user_agent: Optional[str] = None diff --git a/src/lithic/types/wire_party_details.py b/src/lithic/types/wire_party_details.py new file mode 100644 index 00000000..46caa4a2 --- /dev/null +++ b/src/lithic/types/wire_party_details.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["WirePartyDetails"] + + +class WirePartyDetails(BaseModel): + account_number: Optional[str] = None + """Account number""" + + agent_id: Optional[str] = None + """Routing number or BIC of the financial institution""" + + agent_name: Optional[str] = None + """Name of the financial institution""" + + name: Optional[str] = None + """Name of the person or company""" diff --git a/tests/api_resources/test_account_holders.py b/tests/api_resources/test_account_holders.py index ccd60ff8..31ecd345 100644 --- a/tests/api_resources/test_account_holders.py +++ b/tests/api_resources/test_account_holders.py @@ -269,6 +269,124 @@ def test_streaming_response_create_overload_1(self, client: Lithic) -> None: @parametrize def test_method_create_overload_2(self, client: Lithic) -> None: + account_holder = client.account_holders.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + def test_method_create_with_all_params_overload_2(self, client: Lithic) -> None: + account_holder = client.account_holders.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "legal_business_name": "Acme, Inc.", + "dba_business_name": "dba_business_name", + "government_id": "114-123-1513", + "parent_company": "parent_company", + "phone_numbers": ["+15555555555"], + }, + beneficial_owner_individuals=[ + { + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "dob": "1991-03-08 08:00:00", + "email": "tom@middle-earth.com", + "first_name": "Tom", + "government_id": "111-23-1412", + "last_name": "Bombadil", + "phone_number": "+15555555555", + } + ], + control_person={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "dob": "1991-03-08 08:00:00", + "email": "tom@middle-earth.com", + "first_name": "Tom", + "government_id": "111-23-1412", + "last_name": "Bombadil", + "phone_number": "+15555555555", + }, + external_id="external_id", + nature_of_business="Software company selling solutions to the restaurant industry", + tos_timestamp="2022-03-08 08:00:00", + website_url="www.mybusiness.com", + workflow="KYB_DELEGATED", + ) + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + def test_raw_response_create_overload_2(self, client: Lithic) -> None: + response = client.account_holders.with_raw_response.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account_holder = response.parse() + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + def test_streaming_response_create_overload_2(self, client: Lithic) -> None: + with client.account_holders.with_streaming_response.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account_holder = response.parse() + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_create_overload_3(self, client: Lithic) -> None: account_holder = client.account_holders.create( individual={ "address": { @@ -291,7 +409,7 @@ def test_method_create_overload_2(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_method_create_with_all_params_overload_2(self, client: Lithic) -> None: + def test_method_create_with_all_params_overload_3(self, client: Lithic) -> None: account_holder = client.account_holders.create( individual={ "address": { @@ -317,7 +435,7 @@ def test_method_create_with_all_params_overload_2(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_raw_response_create_overload_2(self, client: Lithic) -> None: + def test_raw_response_create_overload_3(self, client: Lithic) -> None: response = client.account_holders.with_raw_response.create( individual={ "address": { @@ -344,7 +462,7 @@ def test_raw_response_create_overload_2(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_streaming_response_create_overload_2(self, client: Lithic) -> None: + def test_streaming_response_create_overload_3(self, client: Lithic) -> None: with client.account_holders.with_streaming_response.create( individual={ "address": { @@ -373,7 +491,7 @@ def test_streaming_response_create_overload_2(self, client: Lithic) -> None: assert cast(Any, response.is_closed) is True @parametrize - def test_method_create_overload_3(self, client: Lithic) -> None: + def test_method_create_overload_4(self, client: Lithic) -> None: account_holder = client.account_holders.create( address={ "address1": "123 Old Forest Way", @@ -392,7 +510,7 @@ def test_method_create_overload_3(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_method_create_with_all_params_overload_3(self, client: Lithic) -> None: + def test_method_create_with_all_params_overload_4(self, client: Lithic) -> None: account_holder = client.account_holders.create( address={ "address1": "123 Old Forest Way", @@ -414,7 +532,7 @@ def test_method_create_with_all_params_overload_3(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_raw_response_create_overload_3(self, client: Lithic) -> None: + def test_raw_response_create_overload_4(self, client: Lithic) -> None: response = client.account_holders.with_raw_response.create( address={ "address1": "123 Old Forest Way", @@ -437,7 +555,7 @@ def test_raw_response_create_overload_3(self, client: Lithic) -> None: assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - def test_streaming_response_create_overload_3(self, client: Lithic) -> None: + def test_streaming_response_create_overload_4(self, client: Lithic) -> None: with client.account_holders.with_streaming_response.create( address={ "address1": "123 Old Forest Way", @@ -1240,6 +1358,124 @@ async def test_streaming_response_create_overload_1(self, async_client: AsyncLit @parametrize async def test_method_create_overload_2(self, async_client: AsyncLithic) -> None: + account_holder = await async_client.account_holders.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + async def test_method_create_with_all_params_overload_2(self, async_client: AsyncLithic) -> None: + account_holder = await async_client.account_holders.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "legal_business_name": "Acme, Inc.", + "dba_business_name": "dba_business_name", + "government_id": "114-123-1513", + "parent_company": "parent_company", + "phone_numbers": ["+15555555555"], + }, + beneficial_owner_individuals=[ + { + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "dob": "1991-03-08 08:00:00", + "email": "tom@middle-earth.com", + "first_name": "Tom", + "government_id": "111-23-1412", + "last_name": "Bombadil", + "phone_number": "+15555555555", + } + ], + control_person={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + "address2": "address2", + }, + "dob": "1991-03-08 08:00:00", + "email": "tom@middle-earth.com", + "first_name": "Tom", + "government_id": "111-23-1412", + "last_name": "Bombadil", + "phone_number": "+15555555555", + }, + external_id="external_id", + nature_of_business="Software company selling solutions to the restaurant industry", + tos_timestamp="2022-03-08 08:00:00", + website_url="www.mybusiness.com", + workflow="KYB_DELEGATED", + ) + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + async def test_raw_response_create_overload_2(self, async_client: AsyncLithic) -> None: + response = await async_client.account_holders.with_raw_response.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account_holder = response.parse() + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + @parametrize + async def test_streaming_response_create_overload_2(self, async_client: AsyncLithic) -> None: + async with async_client.account_holders.with_streaming_response.create( + business_entity={ + "address": { + "address1": "123 Old Forest Way", + "city": "Omaha", + "country": "USA", + "postal_code": "68022", + "state": "NE", + }, + "legal_business_name": "Acme, Inc.", + }, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account_holder = await response.parse() + assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_create_overload_3(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.create( individual={ "address": { @@ -1262,7 +1498,7 @@ async def test_method_create_overload_2(self, async_client: AsyncLithic) -> None assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_method_create_with_all_params_overload_2(self, async_client: AsyncLithic) -> None: + async def test_method_create_with_all_params_overload_3(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.create( individual={ "address": { @@ -1288,7 +1524,7 @@ async def test_method_create_with_all_params_overload_2(self, async_client: Asyn assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_raw_response_create_overload_2(self, async_client: AsyncLithic) -> None: + async def test_raw_response_create_overload_3(self, async_client: AsyncLithic) -> None: response = await async_client.account_holders.with_raw_response.create( individual={ "address": { @@ -1315,7 +1551,7 @@ async def test_raw_response_create_overload_2(self, async_client: AsyncLithic) - assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_streaming_response_create_overload_2(self, async_client: AsyncLithic) -> None: + async def test_streaming_response_create_overload_3(self, async_client: AsyncLithic) -> None: async with async_client.account_holders.with_streaming_response.create( individual={ "address": { @@ -1344,7 +1580,7 @@ async def test_streaming_response_create_overload_2(self, async_client: AsyncLit assert cast(Any, response.is_closed) is True @parametrize - async def test_method_create_overload_3(self, async_client: AsyncLithic) -> None: + async def test_method_create_overload_4(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.create( address={ "address1": "123 Old Forest Way", @@ -1363,7 +1599,7 @@ async def test_method_create_overload_3(self, async_client: AsyncLithic) -> None assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_method_create_with_all_params_overload_3(self, async_client: AsyncLithic) -> None: + async def test_method_create_with_all_params_overload_4(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.create( address={ "address1": "123 Old Forest Way", @@ -1385,7 +1621,7 @@ async def test_method_create_with_all_params_overload_3(self, async_client: Asyn assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_raw_response_create_overload_3(self, async_client: AsyncLithic) -> None: + async def test_raw_response_create_overload_4(self, async_client: AsyncLithic) -> None: response = await async_client.account_holders.with_raw_response.create( address={ "address1": "123 Old Forest Way", @@ -1408,7 +1644,7 @@ async def test_raw_response_create_overload_3(self, async_client: AsyncLithic) - assert_matches_type(AccountHolderCreateResponse, account_holder, path=["response"]) @parametrize - async def test_streaming_response_create_overload_3(self, async_client: AsyncLithic) -> None: + async def test_streaming_response_create_overload_4(self, async_client: AsyncLithic) -> None: async with async_client.account_holders.with_streaming_response.create( address={ "address1": "123 Old Forest Way", diff --git a/tests/test_models.py b/tests/test_models.py index 4ed49592..75384c49 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from pydantic import Field from lithic._utils import PropertyInfo -from lithic._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from lithic._compat import PYDANTIC_V1, parse_obj, model_dump, model_json from lithic._models import BaseModel, construct_type @@ -294,12 +294,12 @@ class Model(BaseModel): assert cast(bool, m.foo) is True m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: + if PYDANTIC_V1: assert isinstance(m.foo, Submodel2) assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore def test_list_of_unions() -> None: @@ -426,10 +426,10 @@ class Model(BaseModel): expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: + if PYDANTIC_V1: expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' model = Model.construct(created_at="2019-12-27T18:11:19.117Z") assert model.created_at == expected @@ -531,7 +531,7 @@ class Model2(BaseModel): assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} assert m4.to_dict(mode="json") == {"created_at": time_str} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -556,7 +556,7 @@ class Model(BaseModel): assert m3.model_dump() == {"foo": None} assert m3.model_dump(exclude_none=True) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) @@ -580,10 +580,10 @@ class Model(BaseModel): assert json.loads(m.to_json()) == {"FOO": "hello"} assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: + if PYDANTIC_V1: assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' m2 = Model() assert json.loads(m2.to_json()) == {} @@ -595,7 +595,7 @@ class Model(BaseModel): assert json.loads(m3.to_json()) == {"FOO": None} assert json.loads(m3.to_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_json(warnings=False) @@ -622,7 +622,7 @@ class Model(BaseModel): assert json.loads(m3.model_dump_json()) == {"foo": None} assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump_json(round_trip=True) @@ -679,12 +679,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_unknown_variant() -> None: @@ -768,12 +768,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: @@ -833,7 +833,7 @@ class B(BaseModel): assert UnionType.__discriminator__ is discriminator -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: Alias = TypeAliasType("Alias", str) # pyright: ignore @@ -849,7 +849,7 @@ class Model(BaseModel): assert m.union == "bar" -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_field_named_cls() -> None: class Model(BaseModel): cls: str @@ -936,7 +936,7 @@ class Type2(BaseModel): assert isinstance(model.value, InnerType2) -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") def test_extra_properties() -> None: class Item(BaseModel): prop: int diff --git a/tests/test_transform.py b/tests/test_transform.py index c5113270..eca75102 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -15,7 +15,7 @@ parse_datetime, async_transform as _async_transform, ) -from lithic._compat import PYDANTIC_V2 +from lithic._compat import PYDANTIC_V1 from lithic._models import BaseModel _T = TypeVar("_T") @@ -189,7 +189,7 @@ class DateModel(BaseModel): @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+00:00" + tz = "+00:00" if PYDANTIC_V1 else "Z" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] @@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_types(use_async: bool) -> None: model = MyModel.construct(foo=True) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": True} @@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_object_type(use_async: bool) -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": {"hello": "world"}} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 00000000..81044b9a --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from lithic._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/utils.py b/tests/utils.py index afd0d680..94ec814d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import inspect import traceback import contextlib -from typing import Any, TypeVar, Iterator, cast +from typing import Any, TypeVar, Iterator, Sequence, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -15,10 +15,11 @@ is_list_type, is_union_type, extract_type_arg, + is_sequence_type, is_annotated_type, is_type_alias_type, ) -from lithic._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from lithic._compat import PYDANTIC_V1, field_outer_type, get_model_fields from lithic._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -27,12 +28,12 @@ def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: for name, field in get_model_fields(model).items(): field_value = getattr(value, name) - if PYDANTIC_V2: - allow_none = False - else: + if PYDANTIC_V1: # in v1 nullability was structured differently # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields allow_none = getattr(field, "allow_none", False) + else: + allow_none = False assert_matches_type( field_outer_type(field), @@ -71,6 +72,13 @@ def assert_matches_type( if is_list_type(type_): return _assert_list_type(type_, value) + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + if origin == str: assert isinstance(value, str) elif origin == int: