From 1bb4e70cc4afa33d8bf1861c31315dfe64eea0d2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:45:25 +0000 Subject: [PATCH 01/10] chore(internal): change default timeout to an int (#687) --- src/lithic/_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lithic/_constants.py b/src/lithic/_constants.py index a2ac3b6f..6ddf2c71 100644 --- a/src/lithic/_constants.py +++ b/src/lithic/_constants.py @@ -6,7 +6,7 @@ OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" # default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) From 2d0fc56ef5c8b93e94a5dab4bf214f75dc8731f3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:26:56 +0000 Subject: [PATCH 02/10] chore(internal): bummp ruff dependency (#689) --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- scripts/utils/ruffen-docs.py | 4 ++-- src/lithic/_models.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edb049dd..a5b16672 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,7 +178,7 @@ select = [ "T201", "T203", # misuse of typing.TYPE_CHECKING - "TCH004", + "TC004", # import rules "TID251", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index a68cd5a9..993d8d76 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -78,7 +78,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.22.0 rich==13.7.1 -ruff==0.6.9 +ruff==0.9.4 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py index 37b3d94f..0cf2bd2f 100644 --- a/scripts/utils/ruffen-docs.py +++ b/scripts/utils/ruffen-docs.py @@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str: with _collect_error(match): code = format_code_block(code) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" def _pycon_match(match: Match[str]) -> str: code = "" @@ -97,7 +97,7 @@ def finish_fragment() -> None: def _md_pycon_match(match: Match[str]) -> str: code = _pycon_match(match) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) diff --git a/src/lithic/_models.py b/src/lithic/_models.py index 9a918aab..12c34b7d 100644 --- a/src/lithic/_models.py +++ b/src/lithic/_models.py @@ -172,7 +172,7 @@ def to_json( @override def __str__(self) -> str: # mypy complains about an invalid self arg - return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] # Override the 'construct' method in a way that supports recursive parsing without validation. # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. From ab6b3e5acb1e2f7614f60441b03348abf4217f19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:36:45 +0000 Subject: [PATCH 03/10] chore(api): new PaymentEventType for ACH Returns and small updates to 3DS AuthenticationResult (#690) - adds `ACH_RETURN_SETTLED` to PaymentEventTypes: https://docs.lithic.com/changelog/january-22-2025 - adds `PENDING_CHALLENGE` and `PENDING_DECISION` to 3DS AuthenticationResult. Updates this field to be required and also adds `challenge_orchestrated_by` property --- src/lithic/resources/payments.py | 2 ++ .../statements/statement_line_items.py | 1 + src/lithic/types/financial_transaction.py | 1 + src/lithic/types/payment.py | 3 ++ .../types/payment_simulate_action_params.py | 1 + .../authentication_retrieve_response.py | 5 +++- src/lithic/types/transaction.py | 30 ++++++++++++------- 7 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/lithic/resources/payments.py b/src/lithic/resources/payments.py index a5602063..f2261cea 100644 --- a/src/lithic/resources/payments.py +++ b/src/lithic/resources/payments.py @@ -267,6 +267,7 @@ def simulate_action( "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", ], decline_reason: Literal[ "PROGRAM_TRANSACTION_LIMIT_EXCEEDED", "PROGRAM_DAILY_LIMIT_EXCEEDED", "PROGRAM_MONTHLY_LIMIT_EXCEEDED" @@ -680,6 +681,7 @@ async def simulate_action( "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", ], decline_reason: Literal[ "PROGRAM_TRANSACTION_LIMIT_EXCEEDED", "PROGRAM_DAILY_LIMIT_EXCEEDED", "PROGRAM_MONTHLY_LIMIT_EXCEEDED" diff --git a/src/lithic/types/financial_accounts/statements/statement_line_items.py b/src/lithic/types/financial_accounts/statements/statement_line_items.py index bc89bc65..a777fa0b 100644 --- a/src/lithic/types/financial_accounts/statements/statement_line_items.py +++ b/src/lithic/types/financial_accounts/statements/statement_line_items.py @@ -50,6 +50,7 @@ class Data(BaseModel): "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", "AUTHORIZATION", "AUTHORIZATION_ADVICE", "AUTHORIZATION_EXPIRY", diff --git a/src/lithic/types/financial_transaction.py b/src/lithic/types/financial_transaction.py index a2fc6ffe..3a4388bf 100644 --- a/src/lithic/types/financial_transaction.py +++ b/src/lithic/types/financial_transaction.py @@ -40,6 +40,7 @@ class Event(BaseModel): "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", "AUTHORIZATION", "AUTHORIZATION_ADVICE", "AUTHORIZATION_EXPIRY", diff --git a/src/lithic/types/payment.py b/src/lithic/types/payment.py index ae75776e..c7da04da 100644 --- a/src/lithic/types/payment.py +++ b/src/lithic/types/payment.py @@ -39,6 +39,7 @@ class Event(BaseModel): "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", ] """Event types: @@ -58,6 +59,8 @@ class Event(BaseModel): - `ACH_RECEIPT_SETTLED` - ACH receipt funds have settled. - `ACH_RECEIPT_RELEASED` - ACH receipt released from pending to available balance. + - `ACH_RETURN_SETTLED` - ACH receipt return settled by the Receiving Depository + Financial Institution. """ detailed_results: Optional[ diff --git a/src/lithic/types/payment_simulate_action_params.py b/src/lithic/types/payment_simulate_action_params.py index 488fe64c..c8d0c128 100644 --- a/src/lithic/types/payment_simulate_action_params.py +++ b/src/lithic/types/payment_simulate_action_params.py @@ -17,6 +17,7 @@ class PaymentSimulateActionParams(TypedDict, total=False): "ACH_RECEIPT_SETTLED", "ACH_RETURN_INITIATED", "ACH_RETURN_PROCESSED", + "ACH_RETURN_SETTLED", ] ] """Event Type""" diff --git a/src/lithic/types/three_ds/authentication_retrieve_response.py b/src/lithic/types/three_ds/authentication_retrieve_response.py index dae4cc66..cfd90a6f 100644 --- a/src/lithic/types/three_ds/authentication_retrieve_response.py +++ b/src/lithic/types/three_ds/authentication_retrieve_response.py @@ -323,7 +323,7 @@ class AuthenticationRetrieveResponse(BaseModel): Maps to EMV 3DS field `acctType`. """ - authentication_result: Optional[Literal["DECLINE", "SUCCESS"]] = None + authentication_result: Literal["DECLINE", "SUCCESS", "PENDING_CHALLENGE", "PENDING_DECISION"] """Indicates the outcome of the 3DS authentication process.""" card_expiry_check: Literal["MATCH", "MISMATCH", "NOT_PRESENT"] @@ -428,6 +428,9 @@ class AuthenticationRetrieveResponse(BaseModel): Present if the channel is 'BROWSER'. """ + challenge_orchestrated_by: Optional[Literal["LITHIC", "CUSTOMER", "NO_CHALLENGE"]] = None + """Entity that orchestrates the challenge.""" + three_ri_request_type: Optional[ Literal[ "ACCOUNT_VERIFICATION", diff --git a/src/lithic/types/transaction.py b/src/lithic/types/transaction.py index 080f1d6b..a95a11fd 100644 --- a/src/lithic/types/transaction.py +++ b/src/lithic/types/transaction.py @@ -389,16 +389,14 @@ class EventNetworkInfoAcquirer(BaseModel): class EventNetworkInfoMastercard(BaseModel): banknet_reference_number: Optional[str] = None - """Identifier assigned by Mastercard.""" + """Identifier assigned by Mastercard. - switch_serial_number: Optional[str] = None - """ - Identifier assigned by Mastercard, applicable to single-message transactions - only. + Guaranteed by Mastercard to be unique for any transaction within a specific + financial network on any processing day. """ original_banknet_reference_number: Optional[str] = None - """[Available on January 28th] Identifier assigned by Mastercard. + """Identifier assigned by Mastercard. Matches the `banknet_reference_number` of a prior related event. May be populated in authorization reversals, incremental authorizations (authorization @@ -413,19 +411,22 @@ class EventNetworkInfoMastercard(BaseModel): """ original_switch_serial_number: Optional[str] = None - """[Available on January 28th] Identifier assigned by Mastercard. + """Identifier assigned by Mastercard. Matches the `switch_serial_number` of a prior related event. May be populated in returns and return reversals. Applicable to single-message transactions only. """ + switch_serial_number: Optional[str] = None + """ + Identifier assigned by Mastercard, applicable to single-message transactions + only. + """ + class EventNetworkInfoVisa(BaseModel): - transaction_id: Optional[str] = None - """Identifier assigned by Visa.""" - original_transaction_id: Optional[str] = None - """[Available on January 28th] Identifier assigned by Visa. + """Identifier assigned by Visa. Matches the `transaction_id` of a prior related event. May be populated in incremental authorizations (authorization requests that augment a previously @@ -433,6 +434,13 @@ class EventNetworkInfoVisa(BaseModel): clearings. """ + transaction_id: Optional[str] = None + """Identifier assigned by Visa to link original messages to subsequent messages. + + Guaranteed by Visa to be unique for each original authorization and financial + authorization. + """ + class EventNetworkInfo(BaseModel): acquirer: Optional[EventNetworkInfoAcquirer] = None From 43d66921603b533dc9348da4f2bd1eb80826ecec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:17:25 +0000 Subject: [PATCH 04/10] feat(client): send `X-Stainless-Read-Timeout` header (#691) --- src/lithic/_base_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lithic/_base_client.py b/src/lithic/_base_client.py index 5e25b373..1ead3872 100644 --- a/src/lithic/_base_client.py +++ b/src/lithic/_base_client.py @@ -419,10 +419,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - # Don't set the retry count header if it was already set or removed by the caller. We check + # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) return headers From ad998734859dcbdae451a38bc26711a854f0edb7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:02:02 +0000 Subject: [PATCH 05/10] chore(internal): fix type traversing dictionary params (#692) --- src/lithic/_utils/_transform.py | 12 +++++++++++- tests/test_transform.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/lithic/_utils/_transform.py b/src/lithic/_utils/_transform.py index a6b62cad..18afd9d8 100644 --- a/src/lithic/_utils/_transform.py +++ b/src/lithic/_utils/_transform.py @@ -25,7 +25,7 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import model_dump, is_typeddict +from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -164,9 +164,14 @@ def _transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return _transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) @@ -307,9 +312,14 @@ async def _async_transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return await _async_transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) diff --git a/tests/test_transform.py b/tests/test_transform.py index 0e6da72d..d92ec53e 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -2,7 +2,7 @@ import io import pathlib -from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast from datetime import date, datetime from typing_extensions import Required, Annotated, TypedDict @@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]: } +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + class TypedDictIterableUnionStr(TypedDict): foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] From 705157bd63c08bfa026647e8f90ab4fc90e46158 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:53:47 +0000 Subject: [PATCH 06/10] feat(pagination): avoid fetching when has_more: false (#693) --- src/lithic/pagination.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lithic/pagination.py b/src/lithic/pagination.py index 7c525eee..73624536 100644 --- a/src/lithic/pagination.py +++ b/src/lithic/pagination.py @@ -26,6 +26,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: is_forwards = not self._options.params.get("ending_before", False) @@ -61,6 +66,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> Optional[PageInfo]: is_forwards = not self._options.params.get("ending_before", False) @@ -96,6 +106,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> None: """ @@ -116,6 +131,11 @@ def _get_page_items(self) -> List[_T]: return [] return data + @override + def has_next_page(self) -> bool: + has_more = self.has_more + return has_more and super().has_next_page() + @override def next_page_info(self) -> None: """ From dfc6046874c04b0873c555f6562208c328e74810 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:24:34 +0000 Subject: [PATCH 07/10] chore(internal): minor type handling changes (#694) --- src/lithic/_models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lithic/_models.py b/src/lithic/_models.py index 12c34b7d..c4401ff8 100644 --- a/src/lithic/_models.py +++ b/src/lithic/_models.py @@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object: If the given value does not match the expected type then it is returned as-is. """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` @@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object: if is_union(origin): try: - return validate_type(type_=cast("type[object]", type_), value=value) + return validate_type(type_=cast("type[object]", original_type or type_), value=value) except Exception: pass From f0dcb605baaa3f89e866182d84e001846428d955 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 06:20:06 +0000 Subject: [PATCH 08/10] chore(api): new 3DS Event and new `challenge_metadata` property on Authentications (#695) - adds `three_ds_authentication.updated` Event - adds `challenge_metadata` to Authentications --- src/lithic/resources/events/events.py | 2 ++ src/lithic/resources/events/subscriptions.py | 6 +++++ .../external_bank_accounts.py | 4 ++++ src/lithic/types/event.py | 1 + src/lithic/types/event_list_params.py | 1 + src/lithic/types/event_subscription.py | 1 + .../events/subscription_create_params.py | 1 + ...scription_send_simulated_example_params.py | 1 + .../events/subscription_update_params.py | 1 + .../external_bank_account_update_params.py | 4 +++- .../authentication_retrieve_response.py | 22 ++++++++++++++----- .../test_external_bank_accounts.py | 2 ++ 12 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/lithic/resources/events/events.py b/src/lithic/resources/events/events.py index 305e81f4..fd206d66 100644 --- a/src/lithic/resources/events/events.py +++ b/src/lithic/resources/events/events.py @@ -130,6 +130,7 @@ def list( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -387,6 +388,7 @@ def list( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/resources/events/subscriptions.py b/src/lithic/resources/events/subscriptions.py index 5f74ecfd..489dcb8d 100644 --- a/src/lithic/resources/events/subscriptions.py +++ b/src/lithic/resources/events/subscriptions.py @@ -97,6 +97,7 @@ def create( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -228,6 +229,7 @@ def update( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -665,6 +667,7 @@ def send_simulated_example( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -772,6 +775,7 @@ async def create( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -903,6 +907,7 @@ async def update( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", @@ -1340,6 +1345,7 @@ async def send_simulated_example( "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", 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 d230dc91..c78f1a55 100644 --- a/src/lithic/resources/external_bank_accounts/external_bank_accounts.py +++ b/src/lithic/resources/external_bank_accounts/external_bank_accounts.py @@ -408,6 +408,7 @@ def update( name: str | NotGiven = NOT_GIVEN, owner: str | NotGiven = NOT_GIVEN, owner_type: OwnerType | NotGiven = NOT_GIVEN, + type: Literal["CHECKING", "SAVINGS"] | NotGiven = NOT_GIVEN, user_defined_id: 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. @@ -460,6 +461,7 @@ def update( "name": name, "owner": owner, "owner_type": owner_type, + "type": type, "user_defined_id": user_defined_id, }, external_bank_account_update_params.ExternalBankAccountUpdateParams, @@ -975,6 +977,7 @@ async def update( name: str | NotGiven = NOT_GIVEN, owner: str | NotGiven = NOT_GIVEN, owner_type: OwnerType | NotGiven = NOT_GIVEN, + type: Literal["CHECKING", "SAVINGS"] | NotGiven = NOT_GIVEN, user_defined_id: 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. @@ -1027,6 +1030,7 @@ async def update( "name": name, "owner": owner, "owner_type": owner_type, + "type": type, "user_defined_id": user_defined_id, }, external_bank_account_update_params.ExternalBankAccountUpdateParams, diff --git a/src/lithic/types/event.py b/src/lithic/types/event.py index 72f8e74d..b6b4a745 100644 --- a/src/lithic/types/event.py +++ b/src/lithic/types/event.py @@ -54,6 +54,7 @@ class Event(BaseModel): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/event_list_params.py b/src/lithic/types/event_list_params.py index 33a059fc..7315dc9f 100644 --- a/src/lithic/types/event_list_params.py +++ b/src/lithic/types/event_list_params.py @@ -66,6 +66,7 @@ class EventListParams(TypedDict, total=False): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/event_subscription.py b/src/lithic/types/event_subscription.py index aa494a13..b0fa87b0 100644 --- a/src/lithic/types/event_subscription.py +++ b/src/lithic/types/event_subscription.py @@ -57,6 +57,7 @@ class EventSubscription(BaseModel): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/events/subscription_create_params.py b/src/lithic/types/events/subscription_create_params.py index 5cb55937..0ee221d9 100644 --- a/src/lithic/types/events/subscription_create_params.py +++ b/src/lithic/types/events/subscription_create_params.py @@ -54,6 +54,7 @@ class SubscriptionCreateParams(TypedDict, total=False): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/events/subscription_send_simulated_example_params.py b/src/lithic/types/events/subscription_send_simulated_example_params.py index 4bea7178..113e87ed 100644 --- a/src/lithic/types/events/subscription_send_simulated_example_params.py +++ b/src/lithic/types/events/subscription_send_simulated_example_params.py @@ -43,6 +43,7 @@ class SubscriptionSendSimulatedExampleParams(TypedDict, total=False): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/events/subscription_update_params.py b/src/lithic/types/events/subscription_update_params.py index b49c7fd3..fd1b7cbd 100644 --- a/src/lithic/types/events/subscription_update_params.py +++ b/src/lithic/types/events/subscription_update_params.py @@ -54,6 +54,7 @@ class SubscriptionUpdateParams(TypedDict, total=False): "settlement_report.updated", "statements.created", "three_ds_authentication.created", + "three_ds_authentication.updated", "tokenization.approval_request", "tokenization.result", "tokenization.two_factor_authentication_code", diff --git a/src/lithic/types/external_bank_account_update_params.py b/src/lithic/types/external_bank_account_update_params.py index c5174891..d20a51e4 100644 --- a/src/lithic/types/external_bank_account_update_params.py +++ b/src/lithic/types/external_bank_account_update_params.py @@ -4,7 +4,7 @@ from typing import Union from datetime import date -from typing_extensions import Annotated, TypedDict +from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo from .owner_type import OwnerType @@ -38,6 +38,8 @@ class ExternalBankAccountUpdateParams(TypedDict, total=False): owner_type: OwnerType """Owner Type""" + type: Literal["CHECKING", "SAVINGS"] + user_defined_id: str """User Defined ID""" diff --git a/src/lithic/types/three_ds/authentication_retrieve_response.py b/src/lithic/types/three_ds/authentication_retrieve_response.py index cfd90a6f..b4ed6923 100644 --- a/src/lithic/types/three_ds/authentication_retrieve_response.py +++ b/src/lithic/types/three_ds/authentication_retrieve_response.py @@ -16,6 +16,7 @@ "AdditionalData", "App", "Browser", + "ChallengeMetadata", "Transaction", ] @@ -276,6 +277,14 @@ class Browser(BaseModel): """Content of the HTTP user-agent header. Maps to EMV 3DS field browserUserAgent.""" +class ChallengeMetadata(BaseModel): + method_type: Literal["SMS_OTP", "OUT_OF_BAND"] + """The type of challenge method used for authentication.""" + + phone_number: Optional[str] = None + """The phone number used for delivering the OTP. Relevant only for SMS_OTP method.""" + + class Transaction(BaseModel): amount: float """Amount of the purchase in minor units of currency with all punctuation removed. @@ -350,11 +359,6 @@ class AuthenticationRetrieveResponse(BaseModel): created: datetime """Date and time when the authentication was created in Lithic's system.""" - decision_made_by: Optional[Literal["CUSTOMER_ENDPOINT", "LITHIC_DEFAULT", "LITHIC_RULES", "NETWORK", "UNKNOWN"]] = ( - None - ) - """Entity that made the authentication decision.""" - merchant: Merchant """ Object containing data about the merchant involved in the e-commerce @@ -428,9 +432,17 @@ class AuthenticationRetrieveResponse(BaseModel): Present if the channel is 'BROWSER'. """ + challenge_metadata: Optional[ChallengeMetadata] = None + """Metadata about the challenge method and delivery.""" + challenge_orchestrated_by: Optional[Literal["LITHIC", "CUSTOMER", "NO_CHALLENGE"]] = None """Entity that orchestrates the challenge.""" + decision_made_by: Optional[Literal["CUSTOMER_ENDPOINT", "LITHIC_DEFAULT", "LITHIC_RULES", "NETWORK", "UNKNOWN"]] = ( + None + ) + """Entity that made the authentication decision.""" + three_ri_request_type: Optional[ Literal[ "ACCOUNT_VERIFICATION", diff --git a/tests/api_resources/test_external_bank_accounts.py b/tests/api_resources/test_external_bank_accounts.py index fe345c9f..bdb409d2 100644 --- a/tests/api_resources/test_external_bank_accounts.py +++ b/tests/api_resources/test_external_bank_accounts.py @@ -311,6 +311,7 @@ def test_method_update_with_all_params(self, client: Lithic) -> None: name="name", owner="owner", owner_type="INDIVIDUAL", + type="CHECKING", user_defined_id="x", ) assert_matches_type(ExternalBankAccountUpdateResponse, external_bank_account, path=["response"]) @@ -775,6 +776,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncLithic) -> name="name", owner="owner", owner_type="INDIVIDUAL", + type="CHECKING", user_defined_id="x", ) assert_matches_type(ExternalBankAccountUpdateResponse, external_bank_account, path=["response"]) From 41d8601b544757e7a7a0769245fbc5ca9142f442 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:47:14 +0000 Subject: [PATCH 09/10] fix: asyncify on non-asyncio runtimes (#696) --- src/lithic/_utils/_sync.py | 19 +++++++++++++++++-- tests/api_resources/test_account_holders.py | 16 ++++++++-------- tests/test_client.py | 10 ++++++---- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/lithic/_utils/_sync.py b/src/lithic/_utils/_sync.py index 8b3aaf2b..ad7ec71b 100644 --- a/src/lithic/_utils/_sync.py +++ b/src/lithic/_utils/_sync.py @@ -7,16 +7,20 @@ from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec +import anyio +import sniffio +import anyio.to_thread + T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") if sys.version_info >= (3, 9): - to_thread = asyncio.to_thread + _asyncio_to_thread = asyncio.to_thread else: # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread # for Python 3.8 support - async def to_thread( + async def _asyncio_to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> Any: """Asynchronously run function *func* in a separate thread. @@ -34,6 +38,17 @@ async def to_thread( return await loop.run_in_executor(None, func_call) +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await _asyncio_to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + # inspired by `asyncer`, https://github.com/tiangolo/asyncer def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ diff --git a/tests/api_resources/test_account_holders.py b/tests/api_resources/test_account_holders.py index 00290d6a..45002dea 100644 --- a/tests/api_resources/test_account_holders.py +++ b/tests/api_resources/test_account_holders.py @@ -720,7 +720,7 @@ def test_path_params_retrieve_document(self, client: Lithic) -> None: @parametrize def test_method_simulate_enrollment_document_review(self, client: Lithic) -> None: account_holder = client.account_holders.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) assert_matches_type(Document, account_holder, path=["response"]) @@ -728,7 +728,7 @@ def test_method_simulate_enrollment_document_review(self, client: Lithic) -> Non @parametrize def test_method_simulate_enrollment_document_review_with_all_params(self, client: Lithic) -> None: account_holder = client.account_holders.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", accepted_entity_status_reasons=["string"], status_reason="DOCUMENT_MISSING_REQUIRED_DATA", @@ -738,7 +738,7 @@ def test_method_simulate_enrollment_document_review_with_all_params(self, client @parametrize def test_raw_response_simulate_enrollment_document_review(self, client: Lithic) -> None: response = client.account_holders.with_raw_response.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) @@ -750,7 +750,7 @@ def test_raw_response_simulate_enrollment_document_review(self, client: Lithic) @parametrize def test_streaming_response_simulate_enrollment_document_review(self, client: Lithic) -> None: with client.account_holders.with_streaming_response.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) as response: assert not response.is_closed @@ -1539,7 +1539,7 @@ async def test_path_params_retrieve_document(self, async_client: AsyncLithic) -> @parametrize async def test_method_simulate_enrollment_document_review(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) assert_matches_type(Document, account_holder, path=["response"]) @@ -1547,7 +1547,7 @@ async def test_method_simulate_enrollment_document_review(self, async_client: As @parametrize async def test_method_simulate_enrollment_document_review_with_all_params(self, async_client: AsyncLithic) -> None: account_holder = await async_client.account_holders.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", accepted_entity_status_reasons=["string"], status_reason="DOCUMENT_MISSING_REQUIRED_DATA", @@ -1557,7 +1557,7 @@ async def test_method_simulate_enrollment_document_review_with_all_params(self, @parametrize async def test_raw_response_simulate_enrollment_document_review(self, async_client: AsyncLithic) -> None: response = await async_client.account_holders.with_raw_response.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) @@ -1569,7 +1569,7 @@ async def test_raw_response_simulate_enrollment_document_review(self, async_clie @parametrize async def test_streaming_response_simulate_enrollment_document_review(self, async_client: AsyncLithic) -> None: async with async_client.account_holders.with_streaming_response.simulate_enrollment_document_review( - document_upload_token="b11cd67b-0a52-4180-8365-314f3def5426", + document_upload_token="document_upload_token", status="UPLOADED", ) as response: assert not response.is_closed diff --git a/tests/test_client.py b/tests/test_client.py index 45c511f4..19ebc227 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,10 +24,12 @@ from lithic import Lithic, AsyncLithic, APIResponseValidationError from lithic._types import Omit +from lithic._utils import maybe_transform from lithic._models import BaseModel, FinalRequestOptions from lithic._constants import RAW_RESPONSE_HEADER from lithic._exceptions import LithicError, APIStatusError, APITimeoutError, APIResponseValidationError from lithic._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options +from lithic.types.card_create_params import CardCreateParams from .utils import update_env @@ -822,7 +824,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/cards", - body=cast(object, dict(type="SINGLE_USE")), + body=cast(object, maybe_transform(dict(type="SINGLE_USE"), CardCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -837,7 +839,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/cards", - body=cast(object, dict(type="SINGLE_USE")), + body=cast(object, maybe_transform(dict(type="SINGLE_USE"), CardCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1740,7 +1742,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/cards", - body=cast(object, dict(type="SINGLE_USE")), + body=cast(object, maybe_transform(dict(type="SINGLE_USE"), CardCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1755,7 +1757,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/cards", - body=cast(object, dict(type="SINGLE_USE")), + body=cast(object, maybe_transform(dict(type="SINGLE_USE"), CardCreateParams)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) From 6f2d9f94fcb5cb31c5b33ee29a70533271761f3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:47:40 +0000 Subject: [PATCH 10/10] release: 0.85.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ pyproject.toml | 2 +- src/lithic/_version.py | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 47dd76c1..ad502a4b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.84.0" + ".": "0.85.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e43a2d28..e0f0abc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.85.0 (2025-02-13) + +Full Changelog: [v0.84.0...v0.85.0](https://github.com/lithic-com/lithic-python/compare/v0.84.0...v0.85.0) + +### Features + +* **client:** send `X-Stainless-Read-Timeout` header ([#691](https://github.com/lithic-com/lithic-python/issues/691)) ([43d6692](https://github.com/lithic-com/lithic-python/commit/43d66921603b533dc9348da4f2bd1eb80826ecec)) +* **pagination:** avoid fetching when has_more: false ([#693](https://github.com/lithic-com/lithic-python/issues/693)) ([705157b](https://github.com/lithic-com/lithic-python/commit/705157bd63c08bfa026647e8f90ab4fc90e46158)) + + +### Bug Fixes + +* asyncify on non-asyncio runtimes ([#696](https://github.com/lithic-com/lithic-python/issues/696)) ([41d8601](https://github.com/lithic-com/lithic-python/commit/41d8601b544757e7a7a0769245fbc5ca9142f442)) + + +### Chores + +* **api:** new 3DS Event and new `challenge_metadata` property on Authentications ([#695](https://github.com/lithic-com/lithic-python/issues/695)) ([f0dcb60](https://github.com/lithic-com/lithic-python/commit/f0dcb605baaa3f89e866182d84e001846428d955)) +* **api:** new PaymentEventType for ACH Returns and small updates to 3DS AuthenticationResult ([#690](https://github.com/lithic-com/lithic-python/issues/690)) ([ab6b3e5](https://github.com/lithic-com/lithic-python/commit/ab6b3e5acb1e2f7614f60441b03348abf4217f19)) +* **internal:** bummp ruff dependency ([#689](https://github.com/lithic-com/lithic-python/issues/689)) ([2d0fc56](https://github.com/lithic-com/lithic-python/commit/2d0fc56ef5c8b93e94a5dab4bf214f75dc8731f3)) +* **internal:** change default timeout to an int ([#687](https://github.com/lithic-com/lithic-python/issues/687)) ([1bb4e70](https://github.com/lithic-com/lithic-python/commit/1bb4e70cc4afa33d8bf1861c31315dfe64eea0d2)) +* **internal:** fix type traversing dictionary params ([#692](https://github.com/lithic-com/lithic-python/issues/692)) ([ad99873](https://github.com/lithic-com/lithic-python/commit/ad998734859dcbdae451a38bc26711a854f0edb7)) +* **internal:** minor type handling changes ([#694](https://github.com/lithic-com/lithic-python/issues/694)) ([dfc6046](https://github.com/lithic-com/lithic-python/commit/dfc6046874c04b0873c555f6562208c328e74810)) + ## 0.84.0 (2025-01-28) Full Changelog: [v0.83.0...v0.84.0](https://github.com/lithic-com/lithic-python/compare/v0.83.0...v0.84.0) diff --git a/pyproject.toml b/pyproject.toml index a5b16672..341248af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lithic" -version = "0.84.0" +version = "0.85.0" description = "The official Python library for the lithic API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/lithic/_version.py b/src/lithic/_version.py index c83f7b7a..23c59b31 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.84.0" # x-release-please-version +__version__ = "0.85.0" # x-release-please-version