diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 58ebf7f2..cadbcc6f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.123.0" + ".": "0.124.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 47b8d19d..277eb3b9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 190 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic/lithic-0f374e78a0212145a2f55a55dfc39a612de19094d5152ae26b1bc74b01b9e343.yml -openapi_spec_hash: ec888cdaebea979a2cd6231ca04c346c -config_hash: 01dfc901bb6d54b0f582155d779bcbe0 +configured_endpoints: 192 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic/lithic-00f07b0edcc0c3c5ef79920ced7f58dac2434df5e4c27ff6041783e8228315f9.yml +openapi_spec_hash: 963688b09480159a06865075c94a2577 +config_hash: 265a2b679964f4ad5706de101ad2a942 diff --git a/CHANGELOG.md b/CHANGELOG.md index 376aab01..e3f92b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.124.0 (2026-05-08) + +Full Changelog: [v0.123.0...v0.124.0](https://github.com/lithic-com/lithic-python/compare/v0.123.0...v0.124.0) + +### Features + +* **api:** add retrieve_signals method to accounts and cards ([6e1649a](https://github.com/lithic-com/lithic-python/commit/6e1649ad7ee7ca58316891747b24c3f4773553d9)) +* **api:** add unit parameter and travel/distance attributes to auth_rules ([02bd0f0](https://github.com/lithic-com/lithic-python/commit/02bd0f021353f6ddb9e930f3e5515c0636adfb29)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([826938a](https://github.com/lithic-com/lithic-python/commit/826938aec1590a333b0e377dc6f99c9e14e96cdf)) + ## 0.123.0 (2026-05-06) Full Changelog: [v0.122.0...v0.123.0](https://github.com/lithic-com/lithic-python/compare/v0.122.0...v0.123.0) diff --git a/api.md b/api.md index 460379dd..2a91f069 100644 --- a/api.md +++ b/api.md @@ -39,6 +39,7 @@ Methods: - client.accounts.retrieve(account_token) -> Account - client.accounts.update(account_token, \*\*params) -> Account - client.accounts.list(\*\*params) -> SyncCursorPage[Account] +- client.accounts.retrieve_signals(account_token) -> SignalsResponse - client.accounts.retrieve_spend_limits(account_token) -> AccountSpendLimits # AccountHolders @@ -88,6 +89,12 @@ Methods: # AuthRules +Types: + +```python +from lithic.types import SignalsResponse +``` + ## V2 Types: @@ -233,6 +240,7 @@ Methods: - client.cards.provision(card_token, \*\*params) -> CardProvisionResponse - client.cards.reissue(card_token, \*\*params) -> Card - client.cards.renew(card_token, \*\*params) -> Card +- client.cards.retrieve_signals(card_token) -> SignalsResponse - client.cards.retrieve_spend_limits(card_token) -> CardSpendLimits - client.cards.search_by_pan(\*\*params) -> Card - client.cards.web_provision(card_token, \*\*params) -> CardWebProvisionResponse diff --git a/pyproject.toml b/pyproject.toml index b9fd106b..5ad1d5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lithic" -version = "0.123.0" +version = "0.124.0" description = "The official Python library for the lithic API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/lithic/_files.py b/src/lithic/_files.py index 0fdce17b..76da9e08 100644 --- a/src/lithic/_files.py +++ b/src/lithic/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files diff --git a/src/lithic/_version.py b/src/lithic/_version.py index 2236ca43..af0c2a6d 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.123.0" # x-release-please-version +__version__ = "0.124.0" # x-release-please-version diff --git a/src/lithic/resources/accounts.py b/src/lithic/resources/accounts.py index 073f1320..b7c3b401 100644 --- a/src/lithic/resources/accounts.py +++ b/src/lithic/resources/accounts.py @@ -18,6 +18,7 @@ from ..pagination import SyncCursorPage, AsyncCursorPage from .._base_client import AsyncPaginator, make_request_options from ..types.account import Account +from ..types.signals_response import SignalsResponse from ..types.account_spend_limits import AccountSpendLimits __all__ = ["Accounts", "AsyncAccounts"] @@ -259,6 +260,46 @@ def list( model=Account, ) + def retrieve_signals( + self, + account_token: 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SignalsResponse: + """ + Returns behavioral feature state derived from an account's transaction history. + + These signals expose the same data used by behavioral rule attributes (e.g. + `AMOUNT_Z_SCORE` with `scope: ACCOUNT`, `IS_NEW_COUNTRY` with `scope: ACCOUNT`) + and custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to + inspect feature values before writing rules and debug rule behavior. + + Note: 3DS fields are not available at the account scope and will be null. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not account_token: + raise ValueError(f"Expected a non-empty value for `account_token` but received {account_token!r}") + return self._get( + path_template("/v1/accounts/{account_token}/signals", account_token=account_token), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SignalsResponse, + ) + def retrieve_spend_limits( self, account_token: str, @@ -533,6 +574,46 @@ def list( model=Account, ) + async def retrieve_signals( + self, + account_token: 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SignalsResponse: + """ + Returns behavioral feature state derived from an account's transaction history. + + These signals expose the same data used by behavioral rule attributes (e.g. + `AMOUNT_Z_SCORE` with `scope: ACCOUNT`, `IS_NEW_COUNTRY` with `scope: ACCOUNT`) + and custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to + inspect feature values before writing rules and debug rule behavior. + + Note: 3DS fields are not available at the account scope and will be null. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not account_token: + raise ValueError(f"Expected a non-empty value for `account_token` but received {account_token!r}") + return await self._get( + path_template("/v1/accounts/{account_token}/signals", account_token=account_token), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SignalsResponse, + ) + async def retrieve_spend_limits( self, account_token: str, @@ -584,6 +665,9 @@ def __init__(self, accounts: Accounts) -> None: self.list = _legacy_response.to_raw_response_wrapper( accounts.list, ) + self.retrieve_signals = _legacy_response.to_raw_response_wrapper( + accounts.retrieve_signals, + ) self.retrieve_spend_limits = _legacy_response.to_raw_response_wrapper( accounts.retrieve_spend_limits, ) @@ -602,6 +686,9 @@ def __init__(self, accounts: AsyncAccounts) -> None: self.list = _legacy_response.async_to_raw_response_wrapper( accounts.list, ) + self.retrieve_signals = _legacy_response.async_to_raw_response_wrapper( + accounts.retrieve_signals, + ) self.retrieve_spend_limits = _legacy_response.async_to_raw_response_wrapper( accounts.retrieve_spend_limits, ) @@ -620,6 +707,9 @@ def __init__(self, accounts: Accounts) -> None: self.list = to_streamed_response_wrapper( accounts.list, ) + self.retrieve_signals = to_streamed_response_wrapper( + accounts.retrieve_signals, + ) self.retrieve_spend_limits = to_streamed_response_wrapper( accounts.retrieve_spend_limits, ) @@ -638,6 +728,9 @@ def __init__(self, accounts: AsyncAccounts) -> None: self.list = async_to_streamed_response_wrapper( accounts.list, ) + self.retrieve_signals = async_to_streamed_response_wrapper( + accounts.retrieve_signals, + ) self.retrieve_spend_limits = async_to_streamed_response_wrapper( accounts.retrieve_spend_limits, ) diff --git a/src/lithic/resources/cards/cards.py b/src/lithic/resources/cards/cards.py index 3bd8eccd..0ab13db6 100644 --- a/src/lithic/resources/cards/cards.py +++ b/src/lithic/resources/cards/cards.py @@ -53,6 +53,7 @@ FinancialTransactionsWithStreamingResponse, AsyncFinancialTransactionsWithStreamingResponse, ) +from ...types.signals_response import SignalsResponse from ...types.card_spend_limits import CardSpendLimits from ...types.spend_limit_duration import SpendLimitDuration from ...types.shared_params.carrier import Carrier @@ -1110,6 +1111,44 @@ def renew( cast_to=Card, ) + def retrieve_signals( + self, + card_token: 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SignalsResponse: + """ + Returns behavioral feature state derived from a card's transaction history. + + These signals expose the same data used by behavioral rule attributes (e.g. + `AMOUNT_Z_SCORE` with `scope: CARD`, `IS_NEW_COUNTRY` with `scope: CARD`) and + custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to inspect + feature values before writing rules and debug rule behavior. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not card_token: + raise ValueError(f"Expected a non-empty value for `card_token` but received {card_token!r}") + return self._get( + path_template("/v1/cards/{card_token}/signals", card_token=card_token), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SignalsResponse, + ) + def retrieve_spend_limits( self, card_token: str, @@ -2300,6 +2339,44 @@ async def renew( cast_to=Card, ) + async def retrieve_signals( + self, + card_token: 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SignalsResponse: + """ + Returns behavioral feature state derived from a card's transaction history. + + These signals expose the same data used by behavioral rule attributes (e.g. + `AMOUNT_Z_SCORE` with `scope: CARD`, `IS_NEW_COUNTRY` with `scope: CARD`) and + custom code `TRANSACTION_HISTORY_SIGNALS` features, allowing clients to inspect + feature values before writing rules and debug rule behavior. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not card_token: + raise ValueError(f"Expected a non-empty value for `card_token` but received {card_token!r}") + return await self._get( + path_template("/v1/cards/{card_token}/signals", card_token=card_token), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SignalsResponse, + ) + async def retrieve_spend_limits( self, card_token: str, @@ -2474,6 +2551,9 @@ def __init__(self, cards: Cards) -> None: self.renew = _legacy_response.to_raw_response_wrapper( cards.renew, ) + self.retrieve_signals = _legacy_response.to_raw_response_wrapper( + cards.retrieve_signals, + ) self.retrieve_spend_limits = _legacy_response.to_raw_response_wrapper( cards.retrieve_spend_limits, ) @@ -2524,6 +2604,9 @@ def __init__(self, cards: AsyncCards) -> None: self.renew = _legacy_response.async_to_raw_response_wrapper( cards.renew, ) + self.retrieve_signals = _legacy_response.async_to_raw_response_wrapper( + cards.retrieve_signals, + ) self.retrieve_spend_limits = _legacy_response.async_to_raw_response_wrapper( cards.retrieve_spend_limits, ) @@ -2574,6 +2657,9 @@ def __init__(self, cards: Cards) -> None: self.renew = to_streamed_response_wrapper( cards.renew, ) + self.retrieve_signals = to_streamed_response_wrapper( + cards.retrieve_signals, + ) self.retrieve_spend_limits = to_streamed_response_wrapper( cards.retrieve_spend_limits, ) @@ -2624,6 +2710,9 @@ def __init__(self, cards: AsyncCards) -> None: self.renew = async_to_streamed_response_wrapper( cards.renew, ) + self.retrieve_signals = async_to_streamed_response_wrapper( + cards.retrieve_signals, + ) self.retrieve_spend_limits = async_to_streamed_response_wrapper( cards.retrieve_spend_limits, ) diff --git a/src/lithic/types/__init__.py b/src/lithic/types/__init__.py index 0f829753..33f2749e 100644 --- a/src/lithic/types/__init__.py +++ b/src/lithic/types/__init__.py @@ -47,6 +47,7 @@ from .hold_list_params import HoldListParams as HoldListParams from .hold_void_params import HoldVoidParams as HoldVoidParams from .kyc_exempt_param import KYCExemptParam as KYCExemptParam +from .signals_response import SignalsResponse as SignalsResponse from .statement_totals import StatementTotals as StatementTotals from .card_embed_params import CardEmbedParams as CardEmbedParams from .card_renew_params import CardRenewParams as CardRenewParams diff --git a/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py b/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py index 3c747d40..4b288ac2 100644 --- a/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py +++ b/src/lithic/types/auth_rules/conditional_authorization_action_parameters.py @@ -11,12 +11,13 @@ class ConditionParameters(BaseModel): - """Additional parameters required for transaction history signal attributes. + """Additional parameters for certain attributes. - Required when - `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, - `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, - `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT`. + Required when `attribute` is one of + `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`, + `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, + `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT` (require `scope`); + or `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other attributes. """ @@ -30,6 +31,16 @@ class ConditionParameters(BaseModel): scope: Optional[Literal["CARD", "ACCOUNT", "BUSINESS_ACCOUNT"]] = None """The entity scope to evaluate the attribute against.""" + unit: Optional[Literal["MPH", "KPH", "MILES", "KILOMETERS"]] = None + """The unit for impossible travel attributes. + + Required when `attribute` is `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION`. + + For `TRAVEL_SPEED`: `MPH` (miles per hour) or `KPH` (kilometers per hour). + + For `DISTANCE_FROM_LAST_TRANSACTION`: `MILES` or `KILOMETERS`. + """ + class Condition(BaseModel): attribute: Literal[ @@ -70,6 +81,8 @@ class Condition(BaseModel): "DISTINCT_COUNTRY_COUNT", "IS_NEW_MERCHANT", "THREE_DS_SUCCESS_RATE", + "TRAVEL_SPEED", + "DISTANCE_FROM_LAST_TRANSACTION", ] """The attribute to target. @@ -173,6 +186,15 @@ class Condition(BaseModel): `parameters` required. - `THREE_DS_SUCCESS_RATE`: The 3DS authentication success rate for the card, as a percentage from 0.0 to 100.0. Card-scoped only; no `parameters` required. + - `TRAVEL_SPEED`: The estimated speed of travel derived from the distance + between the postal code centers of the last card-present transaction and the + current transaction, divided by the elapsed time. Null if there is no prior + card-present transaction, if either postal code cannot be geocoded, or if + elapsed time is zero. Requires `parameters.unit` set to `MPH` or `KPH`. + - `DISTANCE_FROM_LAST_TRANSACTION`: The estimated distance between the postal + code centers of the last card-present transaction and the current transaction. + Null if there is no prior card-present transaction or if either postal code + cannot be geocoded. Requires `parameters.unit` set to `MILES` or `KILOMETERS`. """ operation: ConditionalOperation @@ -182,12 +204,14 @@ class Condition(BaseModel): """A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`""" parameters: Optional[ConditionParameters] = None - """Additional parameters required for transaction history signal attributes. + """Additional parameters for certain attributes. Required when `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, - or `DISTINCT_COUNTRY_COUNT`. Not used for other attributes. + or `DISTINCT_COUNTRY_COUNT` (require `scope`); or `TRAVEL_SPEED` or + `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other + attributes. """ diff --git a/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py b/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py index a70a27bd..e903a5e0 100644 --- a/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py +++ b/src/lithic/types/auth_rules/conditional_authorization_action_parameters_param.py @@ -13,12 +13,13 @@ class ConditionParameters(TypedDict, total=False): - """Additional parameters required for transaction history signal attributes. + """Additional parameters for certain attributes. - Required when - `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, - `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, - `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT`. + Required when `attribute` is one of + `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`, + `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, + `TIME_SINCE_LAST_TRANSACTION`, or `DISTINCT_COUNTRY_COUNT` (require `scope`); + or `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other attributes. """ @@ -32,6 +33,16 @@ class ConditionParameters(TypedDict, total=False): scope: Literal["CARD", "ACCOUNT", "BUSINESS_ACCOUNT"] """The entity scope to evaluate the attribute against.""" + unit: Literal["MPH", "KPH", "MILES", "KILOMETERS"] + """The unit for impossible travel attributes. + + Required when `attribute` is `TRAVEL_SPEED` or `DISTANCE_FROM_LAST_TRANSACTION`. + + For `TRAVEL_SPEED`: `MPH` (miles per hour) or `KPH` (kilometers per hour). + + For `DISTANCE_FROM_LAST_TRANSACTION`: `MILES` or `KILOMETERS`. + """ + class Condition(TypedDict, total=False): attribute: Required[ @@ -73,6 +84,8 @@ class Condition(TypedDict, total=False): "DISTINCT_COUNTRY_COUNT", "IS_NEW_MERCHANT", "THREE_DS_SUCCESS_RATE", + "TRAVEL_SPEED", + "DISTANCE_FROM_LAST_TRANSACTION", ] ] """The attribute to target. @@ -177,6 +190,15 @@ class Condition(TypedDict, total=False): `parameters` required. - `THREE_DS_SUCCESS_RATE`: The 3DS authentication success rate for the card, as a percentage from 0.0 to 100.0. Card-scoped only; no `parameters` required. + - `TRAVEL_SPEED`: The estimated speed of travel derived from the distance + between the postal code centers of the last card-present transaction and the + current transaction, divided by the elapsed time. Null if there is no prior + card-present transaction, if either postal code cannot be geocoded, or if + elapsed time is zero. Requires `parameters.unit` set to `MPH` or `KPH`. + - `DISTANCE_FROM_LAST_TRANSACTION`: The estimated distance between the postal + code centers of the last card-present transaction and the current transaction. + Null if there is no prior card-present transaction or if either postal code + cannot be geocoded. Requires `parameters.unit` set to `MILES` or `KILOMETERS`. """ operation: Required[ConditionalOperation] @@ -186,12 +208,14 @@ class Condition(TypedDict, total=False): """A regex string, to be used with `MATCHES` or `DOES_NOT_MATCH`""" parameters: ConditionParameters - """Additional parameters required for transaction history signal attributes. + """Additional parameters for certain attributes. Required when `attribute` is one of `AMOUNT_Z_SCORE`, `AVG_TRANSACTION_AMOUNT`, `STDEV_TRANSACTION_AMOUNT`, `IS_NEW_COUNTRY`, `IS_NEW_MCC`, `IS_FIRST_TRANSACTION`, `CONSECUTIVE_DECLINES`, `TIME_SINCE_LAST_TRANSACTION`, - or `DISTINCT_COUNTRY_COUNT`. Not used for other attributes. + or `DISTINCT_COUNTRY_COUNT` (require `scope`); or `TRAVEL_SPEED` or + `DISTANCE_FROM_LAST_TRANSACTION` (require `unit`). Not used for other + attributes. """ diff --git a/src/lithic/types/signals_response.py b/src/lithic/types/signals_response.py new file mode 100644 index 00000000..5fc46d6d --- /dev/null +++ b/src/lithic/types/signals_response.py @@ -0,0 +1,186 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["SignalsResponse"] + + +class SignalsResponse(BaseModel): + """ + Behavioral feature state for a card or account derived from its transaction history. + + Derived statistical features (averages, standard deviations, z-scores) are computed using Welford's online algorithm over approved transactions. Average fields are null when fewer than 5 approved transactions have been recorded. Standard deviation fields are null when fewer than 30 approved transactions have been recorded. + + 3DS fields (`three_ds_success_rate`, `three_ds_success_count`, `three_ds_total_count`) are card-scoped and will be null for account responses. + + Raw fields (`seen_countries`, `seen_mccs`, `approved_txn_amount_m2`, etc.) are included so clients can compute their own transaction-specific derivations, such as checking whether a new transaction's country is in `seen_countries` to determine `is_new_country`, or computing a z-score using the raw mean and M2 values. + """ + + approved_txn_amount_m2: Optional[float] = None + """The Welford M2 accumulator for lifetime approved transaction amounts. + + Used together with `avg_transaction_amount` and `approved_txn_count` to compute + the z-score of a new transaction amount (variance = M2 / (count - 1)). + """ + + approved_txn_amount_m2_30d: Optional[float] = None + """ + The Welford M2 accumulator for approved transaction amounts over the last 30 + days. + """ + + approved_txn_amount_m2_7d: Optional[float] = None + """ + The Welford M2 accumulator for approved transaction amounts over the last 7 + days. + """ + + approved_txn_amount_m2_90d: Optional[float] = None + """ + The Welford M2 accumulator for approved transaction amounts over the last 90 + days. + """ + + approved_txn_count: Optional[int] = None + """The total number of approved transactions over the entity's lifetime.""" + + approved_txn_count_30d: Optional[int] = None + """The number of approved transactions in the last 30 days.""" + + approved_txn_count_7d: Optional[int] = None + """The number of approved transactions in the last 7 days.""" + + approved_txn_count_90d: Optional[int] = None + """The number of approved transactions in the last 90 days.""" + + avg_transaction_amount: Optional[float] = None + """The average approved transaction amount over the entity's lifetime, in cents. + + Null if fewer than 5 approved transactions have been recorded. + """ + + avg_transaction_amount_30d: Optional[float] = None + """The average approved transaction amount over the last 30 days, in cents. + + Null if fewer than 5 approved transactions in window. + """ + + avg_transaction_amount_7d: Optional[float] = None + """The average approved transaction amount over the last 7 days, in cents. + + Null if fewer than 5 approved transactions in window. + """ + + avg_transaction_amount_90d: Optional[float] = None + """The average approved transaction amount over the last 90 days, in cents. + + Null if fewer than 5 approved transactions in window. + """ + + distinct_country_count: Optional[int] = None + """ + The number of distinct merchant countries seen in the entity's transaction + history. + """ + + distinct_mcc_count: Optional[int] = None + """The number of distinct MCCs seen in the entity's transaction history.""" + + first_txn_at: Optional[datetime] = None + """ + The timestamp of the first approved transaction for the entity, in ISO 8601 + format. + """ + + is_first_transaction: Optional[bool] = None + """Whether the entity has no prior transaction history. + + Returns true if no history is found. Null if transaction history exists but a + first transaction timestamp is unavailable. + """ + + last_cp_country: Optional[str] = None + """The merchant country of the last card-present transaction.""" + + last_cp_postal_code: Optional[str] = None + """The merchant postal code of the last card-present transaction.""" + + last_cp_timestamp: Optional[datetime] = None + """The timestamp of the last card-present transaction, in ISO 8601 format.""" + + last_txn_approved_at: Optional[datetime] = None + """ + The timestamp of the most recent approved transaction for the entity, in ISO + 8601 format. + """ + + seen_countries: Optional[List[str]] = None + """The set of merchant countries seen in the entity's transaction history. + + Clients can use this to determine whether a new transaction's country is novel + (i.e. compute `is_new_country`). + """ + + seen_mccs: Optional[List[str]] = None + """The set of MCCs seen in the entity's transaction history. + + Clients can use this to determine whether a new transaction's MCC is novel (i.e. + compute `is_new_mcc`). + """ + + seen_merchants: Optional[List[str]] = None + """ + The set of card acceptor IDs seen in the card's approved transaction history, + capped at the 1000 most recently seen. Null for account responses. Clients can + use this to determine whether a new transaction's merchant is novel (i.e. + compute `is_new_merchant`). + """ + + stdev_transaction_amount: Optional[float] = None + """ + The standard deviation of approved transaction amounts over the entity's + lifetime, in cents. Null if fewer than 30 approved transactions have been + recorded. + """ + + stdev_transaction_amount_30d: Optional[float] = None + """ + The standard deviation of approved transaction amounts over the last 30 days, in + cents. Null if fewer than 30 approved transactions in window. + """ + + stdev_transaction_amount_7d: Optional[float] = None + """ + The standard deviation of approved transaction amounts over the last 7 days, in + cents. Null if fewer than 30 approved transactions in window. + """ + + stdev_transaction_amount_90d: Optional[float] = None + """ + The standard deviation of approved transaction amounts over the last 90 days, in + cents. Null if fewer than 30 approved transactions in window. + """ + + three_ds_success_count: Optional[int] = None + """The number of successful 3DS authentications for the card. + + Null for account responses. + """ + + three_ds_success_rate: Optional[float] = None + """ + The 3DS authentication success rate for the card, as a percentage from 0.0 to + 100.0. Null for account responses. + """ + + three_ds_total_count: Optional[int] = None + """The total number of 3DS authentication attempts for the card. + + Null for account responses. + """ + + time_since_last_transaction_days: Optional[float] = None + """The number of days since the last approved transaction on the entity.""" diff --git a/tests/api_resources/test_accounts.py b/tests/api_resources/test_accounts.py index e9938613..2111305b 100644 --- a/tests/api_resources/test_accounts.py +++ b/tests/api_resources/test_accounts.py @@ -9,7 +9,7 @@ from lithic import Lithic, AsyncLithic from tests.utils import assert_matches_type -from lithic.types import Account, AccountSpendLimits +from lithic.types import Account, SignalsResponse, AccountSpendLimits from lithic._utils import parse_datetime from lithic.pagination import SyncCursorPage, AsyncCursorPage @@ -157,6 +157,44 @@ def test_streaming_response_list(self, client: Lithic) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_retrieve_signals(self, client: Lithic) -> None: + account = client.accounts.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SignalsResponse, account, path=["response"]) + + @parametrize + def test_raw_response_retrieve_signals(self, client: Lithic) -> None: + response = client.accounts.with_raw_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(SignalsResponse, account, path=["response"]) + + @parametrize + def test_streaming_response_retrieve_signals(self, client: Lithic) -> None: + with client.accounts.with_streaming_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account = response.parse() + assert_matches_type(SignalsResponse, account, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve_signals(self, client: Lithic) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_token` but received ''"): + client.accounts.with_raw_response.retrieve_signals( + "", + ) + @parametrize def test_method_retrieve_spend_limits(self, client: Lithic) -> None: account = client.accounts.retrieve_spend_limits( @@ -339,6 +377,44 @@ async def test_streaming_response_list(self, async_client: AsyncLithic) -> None: assert cast(Any, response.is_closed) is True + @parametrize + async def test_method_retrieve_signals(self, async_client: AsyncLithic) -> None: + account = await async_client.accounts.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SignalsResponse, account, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_signals(self, async_client: AsyncLithic) -> None: + response = await async_client.accounts.with_raw_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(SignalsResponse, account, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_signals(self, async_client: AsyncLithic) -> None: + async with async_client.accounts.with_streaming_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account = await response.parse() + assert_matches_type(SignalsResponse, account, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve_signals(self, async_client: AsyncLithic) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `account_token` but received ''"): + await async_client.accounts.with_raw_response.retrieve_signals( + "", + ) + @parametrize async def test_method_retrieve_spend_limits(self, async_client: AsyncLithic) -> None: account = await async_client.accounts.retrieve_spend_limits( diff --git a/tests/api_resources/test_cards.py b/tests/api_resources/test_cards.py index e7f30b11..1e289d72 100644 --- a/tests/api_resources/test_cards.py +++ b/tests/api_resources/test_cards.py @@ -13,6 +13,7 @@ Card, NonPCICard, CardSpendLimits, + SignalsResponse, CardProvisionResponse, CardWebProvisionResponse, ) @@ -580,6 +581,44 @@ def test_path_params_renew(self, client: Lithic) -> None: }, ) + @parametrize + def test_method_retrieve_signals(self, client: Lithic) -> None: + card = client.cards.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SignalsResponse, card, path=["response"]) + + @parametrize + def test_raw_response_retrieve_signals(self, client: Lithic) -> None: + response = client.cards.with_raw_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + card = response.parse() + assert_matches_type(SignalsResponse, card, path=["response"]) + + @parametrize + def test_streaming_response_retrieve_signals(self, client: Lithic) -> None: + with client.cards.with_streaming_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + card = response.parse() + assert_matches_type(SignalsResponse, card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve_signals(self, client: Lithic) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `card_token` but received ''"): + client.cards.with_raw_response.retrieve_signals( + "", + ) + @parametrize def test_method_retrieve_spend_limits(self, client: Lithic) -> None: card = client.cards.retrieve_spend_limits( @@ -1259,6 +1298,44 @@ async def test_path_params_renew(self, async_client: AsyncLithic) -> None: }, ) + @parametrize + async def test_method_retrieve_signals(self, async_client: AsyncLithic) -> None: + card = await async_client.cards.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(SignalsResponse, card, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_signals(self, async_client: AsyncLithic) -> None: + response = await async_client.cards.with_raw_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + card = response.parse() + assert_matches_type(SignalsResponse, card, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_signals(self, async_client: AsyncLithic) -> None: + async with async_client.cards.with_streaming_response.retrieve_signals( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + card = await response.parse() + assert_matches_type(SignalsResponse, card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve_signals(self, async_client: AsyncLithic) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `card_token` but received ''"): + await async_client.cards.with_raw_response.retrieve_signals( + "", + ) + @parametrize async def test_method_retrieve_spend_limits(self, async_client: AsyncLithic) -> None: card = await async_client.cards.retrieve_spend_limits(