diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 65b6e7b4..37e99723 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.129.0" + ".": "0.130.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc3a9f5..b886f450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.130.0](https://github.com/kernel/kernel-python-sdk/compare/v0.129.0...v0.130.0) (2026-10-24) + + +### Features + +* Fall back to aliases after network token enrollment failures ([5930892](https://github.com/kernel/kernel-python-sdk/commit/5930892a91a3f212f27dfdaab4412bf972a4973f)) + + +### Bug Fixes + +* continue offset pagination past empty pages ([80908e4](https://github.com/kernel/kernel-python-sdk/commit/80908e4a5fae05aedc1db65f67b70a34781273d2)) +* follow offset cursors in either direction ([bfcfa9a](https://github.com/kernel/kernel-python-sdk/commit/bfcfa9aac5a1420fced2960757ce80f197be257a)) + ## [0.129.0](https://github.com/kernel/kernel-python-sdk/compare/v0.128.0...v0.129.0) (2026-10-21) diff --git a/api.md b/api.md index 5463b586..67038a5c 100644 --- a/api.md +++ b/api.md @@ -602,6 +602,8 @@ from kernel.types.vaults import ( KernelCredentialVaultItemState, KernelWalletState, KernelWalletVaultItemSpec, + LinkShippingAddress, + LinkUserInfo, ManagedAuthCredentialVaultField, ManagedAuthCredentialVaultItemSpec, ManagedAuthCredentialVaultItemSpecInput, diff --git a/pyproject.toml b/pyproject.toml index a8eaee08..afa56013 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.129.0" +version = "0.130.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 5adecbb7..bcb0d447 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.129.0" # x-release-please-version +__version__ = "0.130.0" # x-release-please-version diff --git a/src/kernel/pagination.py b/src/kernel/pagination.py index 98152da2..67adcea9 100644 --- a/src/kernel/pagination.py +++ b/src/kernel/pagination.py @@ -114,7 +114,7 @@ def has_next_page(self) -> bool: if has_more is not None and has_more is False: return False - return super().has_next_page() + return self.next_page_info() is not None @override def next_page_info(self) -> Optional[PageInfo]: @@ -133,6 +133,12 @@ def next_page_info(self) -> Optional[PageInfo]: if next_offset == 0: return None + # Cursors can move in either direction. Stop only when the server repeats + # the cursor and would re-request the same page. + request_offset = self._options.params.get("offset") + if isinstance(request_offset, int) and next_offset == request_offset: + return None + return PageInfo(params={"offset": next_offset}) @classmethod @@ -165,7 +171,7 @@ def has_next_page(self) -> bool: if has_more is not None and has_more is False: return False - return super().has_next_page() + return self.next_page_info() is not None @override def next_page_info(self) -> Optional[PageInfo]: @@ -184,6 +190,12 @@ def next_page_info(self) -> Optional[PageInfo]: if next_offset == 0: return None + # Cursors can move in either direction. Stop only when the server repeats + # the cursor and would re-request the same page. + request_offset = self._options.params.get("offset") + if isinstance(request_offset, int) and next_offset == request_offset: + return None + return PageInfo(params={"offset": next_offset}) @classmethod diff --git a/src/kernel/resources/vaults/items.py b/src/kernel/resources/vaults/items.py index 9829d498..90484496 100644 --- a/src/kernel/resources/vaults/items.py +++ b/src/kernel/resources/vaults/items.py @@ -66,7 +66,7 @@ def retrieve( key: str, *, id_or_name: str, - expand: List[Literal["payment_methods"]] | Omit = omit, + expand: List[Literal["payment_methods", "link_user_info", "link_shipping_addresses"]] | Omit = omit, wait: int | Omit = omit, # 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. @@ -746,7 +746,8 @@ def perform_operation( prefix or glob matching. When an item allows omitting it, the browser must have exactly one open page. - timeout_ms: Total operation deadline in milliseconds, not a per-field timeout. + timeout_ms: Ignored. Fill sets no operation deadline; typing pace adapts to the length of + the values instead. Accepted for compatibility. extra_headers: Send extra headers @@ -1447,7 +1448,7 @@ async def retrieve( key: str, *, id_or_name: str, - expand: List[Literal["payment_methods"]] | Omit = omit, + expand: List[Literal["payment_methods", "link_user_info", "link_shipping_addresses"]] | Omit = omit, wait: int | Omit = omit, # 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. @@ -2127,7 +2128,8 @@ async def perform_operation( prefix or glob matching. When an item allows omitting it, the browser must have exactly one open page. - timeout_ms: Total operation deadline in milliseconds, not a per-field timeout. + timeout_ms: Ignored. Fill sets no operation deadline; typing pace adapts to the length of + the values instead. Accepted for compatibility. extra_headers: Send extra headers diff --git a/src/kernel/types/vaults/__init__.py b/src/kernel/types/vaults/__init__.py index d72d730c..1fc9c4fc 100644 --- a/src/kernel/types/vaults/__init__.py +++ b/src/kernel/types/vaults/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from .vault_item import VaultItem as VaultItem +from .link_user_info import LinkUserInfo as LinkUserInfo from .vault_item_event import VaultItemEvent as VaultItemEvent from .kernel_card_state import KernelCardState as KernelCardState from .vault_item_action import VaultItemAction as VaultItemAction @@ -18,6 +19,7 @@ from .vault_payment_method import VaultPaymentMethod as VaultPaymentMethod from .card_vault_item_state import CardVaultItemState as CardVaultItemState from .credential_vault_item import CredentialVaultItem as CredentialVaultItem +from .link_shipping_address import LinkShippingAddress as LinkShippingAddress from .vault_fill_field_param import VaultFillFieldParam as VaultFillFieldParam from .wallet_vault_item_spec import WalletVaultItemSpec as WalletVaultItemSpec from .vault_fill_field_result import VaultFillFieldResult as VaultFillFieldResult diff --git a/src/kernel/types/vaults/card_vault_item_spec.py b/src/kernel/types/vaults/card_vault_item_spec.py index 749bf1d4..77a8a508 100644 --- a/src/kernel/types/vaults/card_vault_item_spec.py +++ b/src/kernel/types/vaults/card_vault_item_spec.py @@ -12,6 +12,7 @@ "LinkCardVaultItemSpec", "LinkCardVaultItemSpecLineItem", "LinkCardVaultItemSpecLineItemTotal", + "LinkCardVaultItemSpecRecurring", "LinkCardVaultItemSpecTotal", "AgentCardCardVaultItemSpec", ] @@ -47,6 +48,18 @@ class LinkCardVaultItemSpecLineItem(BaseModel): url: Optional[str] = None +class LinkCardVaultItemSpecRecurring(BaseModel): + """Recurring terms for a subscription or other repeated purchase. + + The amount becomes the spending limit for each period of interval_count intervals, for example every 2 weeks. The merchant schedules charges. The card stays ready after each charge until it expires or is deleted; do not create a new card each period. Deleting the card, its wallet or its vault first cancels the credential with Link, so later charges decline; if Link cannot be reached, deletion returns 409 provider_revocation_pending and should be retried. Deletion does not cancel the subscription with the merchant. Recurring terms cannot be changed once the card is authorized. + """ + + interval: Literal["day", "week", "month", "year"] + + interval_count: Optional[int] = None + """Number of intervals per period. Link defaults it to 1.""" + + class LinkCardVaultItemSpecTotal(BaseModel): amount: int """Total amount in minor currency units.""" @@ -63,7 +76,10 @@ class LinkCardVaultItemSpec(BaseModel): """ amount: int - """Integer amount in minor currency units, at most 50000.""" + """Integer amount in minor currency units, at most 50000. + + With recurring, the spending limit per period, not a lifetime total. + """ context: str @@ -89,6 +105,19 @@ class LinkCardVaultItemSpec(BaseModel): metadata: Optional[Dict[str, str]] = None + recurring: Optional[LinkCardVaultItemSpecRecurring] = None + """Recurring terms for a subscription or other repeated purchase. + + The amount becomes the spending limit for each period of interval_count + intervals, for example every 2 weeks. The merchant schedules charges. The card + stays ready after each charge until it expires or is deleted; do not create a + new card each period. Deleting the card, its wallet or its vault first cancels + the credential with Link, so later charges decline; if Link cannot be reached, + deletion returns 409 provider_revocation_pending and should be retried. Deletion + does not cancel the subscription with the merchant. Recurring terms cannot be + changed once the card is authorized. + """ + totals: Optional[List[LinkCardVaultItemSpecTotal]] = None diff --git a/src/kernel/types/vaults/card_vault_item_spec_param.py b/src/kernel/types/vaults/card_vault_item_spec_param.py index c888247f..45825f2a 100644 --- a/src/kernel/types/vaults/card_vault_item_spec_param.py +++ b/src/kernel/types/vaults/card_vault_item_spec_param.py @@ -12,6 +12,7 @@ "LinkCardVaultItemSpec", "LinkCardVaultItemSpecLineItem", "LinkCardVaultItemSpecLineItemTotal", + "LinkCardVaultItemSpecRecurring", "LinkCardVaultItemSpecTotal", "AgentCardCardVaultItemSpec", ] @@ -47,6 +48,18 @@ class LinkCardVaultItemSpecLineItem(TypedDict, total=False): url: str +class LinkCardVaultItemSpecRecurring(TypedDict, total=False): + """Recurring terms for a subscription or other repeated purchase. + + The amount becomes the spending limit for each period of interval_count intervals, for example every 2 weeks. The merchant schedules charges. The card stays ready after each charge until it expires or is deleted; do not create a new card each period. Deleting the card, its wallet or its vault first cancels the credential with Link, so later charges decline; if Link cannot be reached, deletion returns 409 provider_revocation_pending and should be retried. Deletion does not cancel the subscription with the merchant. Recurring terms cannot be changed once the card is authorized. + """ + + interval: Required[Literal["day", "week", "month", "year"]] + + interval_count: int + """Number of intervals per period. Link defaults it to 1.""" + + class LinkCardVaultItemSpecTotal(TypedDict, total=False): amount: Required[int] """Total amount in minor currency units.""" @@ -63,7 +76,10 @@ class LinkCardVaultItemSpec(TypedDict, total=False): """ amount: Required[int] - """Integer amount in minor currency units, at most 50000.""" + """Integer amount in minor currency units, at most 50000. + + With recurring, the spending limit per period, not a lifetime total. + """ context: Required[str] @@ -89,6 +105,19 @@ class LinkCardVaultItemSpec(TypedDict, total=False): metadata: Dict[str, str] + recurring: LinkCardVaultItemSpecRecurring + """Recurring terms for a subscription or other repeated purchase. + + The amount becomes the spending limit for each period of interval_count + intervals, for example every 2 weeks. The merchant schedules charges. The card + stays ready after each charge until it expires or is deleted; do not create a + new card each period. Deleting the card, its wallet or its vault first cancels + the credential with Link, so later charges decline; if Link cannot be reached, + deletion returns 409 provider_revocation_pending and should be retried. Deletion + does not cancel the subscription with the merchant. Recurring terms cannot be + changed once the card is authorized. + """ + totals: Iterable[LinkCardVaultItemSpecTotal] diff --git a/src/kernel/types/vaults/credential_account_vault_item.py b/src/kernel/types/vaults/credential_account_vault_item.py index a049c193..1440a280 100644 --- a/src/kernel/types/vaults/credential_account_vault_item.py +++ b/src/kernel/types/vaults/credential_account_vault_item.py @@ -19,7 +19,7 @@ class AvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class AvailableOperation(BaseModel): diff --git a/src/kernel/types/vaults/credential_vault_item.py b/src/kernel/types/vaults/credential_vault_item.py index 91f6d8e1..3749bd85 100644 --- a/src/kernel/types/vaults/credential_vault_item.py +++ b/src/kernel/types/vaults/credential_vault_item.py @@ -26,7 +26,7 @@ class AvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class AvailableOperation(BaseModel): diff --git a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py index a86966d1..83fb5b1a 100644 --- a/src/kernel/types/vaults/fill_vault_item_operation_request_param.py +++ b/src/kernel/types/vaults/fill_vault_item_operation_request_param.py @@ -57,4 +57,8 @@ class FillVaultItemOperationRequestParam(TypedDict, total=False): """ timeout_ms: int - """Total operation deadline in milliseconds, not a per-field timeout.""" + """Ignored. + + Fill sets no operation deadline; typing pace adapts to the length of the values + instead. Accepted for compatibility. + """ diff --git a/src/kernel/types/vaults/item_perform_operation_params.py b/src/kernel/types/vaults/item_perform_operation_params.py index 9c3cdbf1..65b3ff7d 100644 --- a/src/kernel/types/vaults/item_perform_operation_params.py +++ b/src/kernel/types/vaults/item_perform_operation_params.py @@ -121,7 +121,11 @@ class FillVaultItemOperationRequest(TypedDict, total=False): """ timeout_ms: int - """Total operation deadline in milliseconds, not a per-field timeout.""" + """Ignored. + + Fill sets no operation deadline; typing pace adapts to the length of the values + instead. Accepted for compatibility. + """ class OnePasswordRequestAccessVaultItemOperationRequest(TypedDict, total=False): diff --git a/src/kernel/types/vaults/item_retrieve_params.py b/src/kernel/types/vaults/item_retrieve_params.py index 1903a36a..4356f656 100644 --- a/src/kernel/types/vaults/item_retrieve_params.py +++ b/src/kernel/types/vaults/item_retrieve_params.py @@ -11,7 +11,7 @@ class ItemRetrieveParams(TypedDict, total=False): id_or_name: Required[str] - expand: List[Literal["payment_methods"]] + expand: List[Literal["payment_methods", "link_user_info", "link_shipping_addresses"]] """Live fields advertised by `available_expansions` to include in `expanded`.""" wait: int diff --git a/src/kernel/types/vaults/kernel_card_state.py b/src/kernel/types/vaults/kernel_card_state.py index b2daa14f..e4732f2a 100644 --- a/src/kernel/types/vaults/kernel_card_state.py +++ b/src/kernel/types/vaults/kernel_card_state.py @@ -43,8 +43,11 @@ class KernelCardState(BaseModel): "requested", "pending_authorization", "ready", "consumed", "expired", "declined", "recovery_required" ] """ - pending_authorization on a Visa purchase waits for the cardholder to approve it - through the spend_approval action; an unused link expires after 30 minutes. + pending_authorization waits for cardholder approval on Visa purchases or fresh + security-code collection on alias-mode purchases through the spend_approval + action; an unused link expires after 30 minutes. Alias-mode cards become ready + after collection and expire at the temporary security-code alias deadline, by + default five minutes later. An expired alias purchase requires a new card. recovery_required means verification, approving the purchase or issuing the one-time code has an unresolved outcome. A server-classified initial attestation failure before intent dispatch may advertise restart_verification to prepare a diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec.py b/src/kernel/types/vaults/kernel_card_vault_item_spec.py index 77912828..d6c2c93c 100644 --- a/src/kernel/types/vaults/kernel_card_vault_item_spec.py +++ b/src/kernel/types/vaults/kernel_card_vault_item_spec.py @@ -11,7 +11,7 @@ class KernelCardVaultItemSpec(BaseModel): """One live purchase with a Kernel-enrolled card. - Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize prepares checkout details without issuing a one-time code. The card can be filled into one checkout before its expires_at, one hour after authorization. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead. + Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize returns pending_authorization with a spend_approval action to collect a fresh security code in a hosted field. Once collected, the card is ready and can be filled before its expires_at, by default five minutes after collection. An expired card requires a new purchase. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead. """ amount: int diff --git a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py index 77d3084d..e836f734 100644 --- a/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py +++ b/src/kernel/types/vaults/kernel_card_vault_item_spec_param.py @@ -10,7 +10,7 @@ class KernelCardVaultItemSpecParam(TypedDict, total=False): """One live purchase with a Kernel-enrolled card. - Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize prepares checkout details without issuing a one-time code. The card can be filled into one checkout before its expires_at, one hour after authorization. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead. + Authorization obtains an agentic network token number, expiry and one-time 3-digit code. They are stored encrypted for the fill operation, which types them only on merchant_url's origin; the merchant's own checkout submits the payment. The one-time code is valid until the item's expires_at; fill and submit checkout before then. Mastercard purchases need no cardholder approval. A Visa purchase returns a spend_approval action: the cardholder approves it with a Visa passkey, and Kernel registers a Visa intent for one transaction up to the amount before issuing the code. For supported cards whose network cannot be enrolled for agentic tokens, authorize returns pending_authorization with a spend_approval action to collect a fresh security code in a hosted field. Once collected, the card is ready and can be filled before its expires_at, by default five minutes after collection. An expired card requires a new purchase. Fill types those details on merchant_url's origin. The amount, merchant_url and merchant_country are recorded but do not restrict payment requests sent by the browser. Card updates are not supported; delete and create a new item instead. """ amount: Required[int] diff --git a/src/kernel/types/vaults/link_shipping_address.py b/src/kernel/types/vaults/link_shipping_address.py new file mode 100644 index 00000000..1a8f52ef --- /dev/null +++ b/src/kernel/types/vaults/link_shipping_address.py @@ -0,0 +1,53 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["LinkShippingAddress", "Address"] + + +class Address(BaseModel): + """Link's nullable postal address fields. + + Omitted from the shipping address when Link reports null. + """ + + administrative_area: Optional[str] = None + """State, province or region.""" + + country_code: Optional[str] = None + + dependent_locality: Optional[str] = None + + line_1: Optional[str] = None + + line_2: Optional[str] = None + + locality: Optional[str] = None + """City or town.""" + + name: Optional[str] = None + + postal_code: Optional[str] = None + + sorting_code: Optional[str] = None + + +class LinkShippingAddress(BaseModel): + """ + One entry of Link's `GET /shipping_addresses` response, passed through as Link reports it. Kernel never stores it. + """ + + id: str + """Link shipping address ID.""" + + is_default: bool + + nickname: Optional[str] = None + + address: Optional[Address] = None + """Link's nullable postal address fields. + + Omitted from the shipping address when Link reports null. + """ diff --git a/src/kernel/types/vaults/link_user_info.py b/src/kernel/types/vaults/link_user_info.py new file mode 100644 index 00000000..bbcac3b8 --- /dev/null +++ b/src/kernel/types/vaults/link_user_info.py @@ -0,0 +1,115 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = [ + "LinkUserInfo", + "Address", + "AgentWalletSpendLimits", + "AgentWalletSpendLimitsDaily", + "AgentWalletSpendLimitsPerTransaction", + "AgentWalletSpendLimitsThirtyDay", + "AgentWalletStepUp", +] + + +class Address(BaseModel): + """The Link user's postal address. Omitted when Link reports none.""" + + city: Optional[str] = None + + country: Optional[str] = None + + line1: Optional[str] = None + + line2: Optional[str] = None + + postal_code: Optional[str] = None + + state: Optional[str] = None + + +class AgentWalletSpendLimitsDaily(BaseModel): + limit: Optional[int] = None + """Null means unlimited.""" + + remaining: Optional[int] = None + """Amount still available in the rolling period; null means unlimited.""" + + used: int + """Amount already spent in the rolling period.""" + + +class AgentWalletSpendLimitsPerTransaction(BaseModel): + limit: Optional[int] = None + """Null means unlimited.""" + + +class AgentWalletSpendLimitsThirtyDay(BaseModel): + limit: Optional[int] = None + """Null means unlimited.""" + + remaining: Optional[int] = None + """Amount still available in the rolling period; null means unlimited.""" + + used: int + """Amount already spent in the rolling period.""" + + +class AgentWalletSpendLimits(BaseModel): + """Link's agent wallet spend limits, per transaction and over rolling periods.""" + + daily: AgentWalletSpendLimitsDaily + + per_transaction: AgentWalletSpendLimitsPerTransaction + + thirty_day: AgentWalletSpendLimitsThirtyDay + + +class AgentWalletStepUp(BaseModel): + """Verification Link requires from the user before the agent wallet can spend.""" + + action_url: Optional[str] = None + """URL where the user completes verification; null when no action is available.""" + + status: str + """ + Link status such as not_required, ssn_verification, identity_verification, + contact_support or complete. + """ + + +class LinkUserInfo(BaseModel): + """ + Link's `GET /userinfo` response for the connected wallet, passed through as Link reports it when the `link_user_info` expansion is requested. Kernel never stores it. Null fields are null at Link; omitted fields were not reported. Amounts are in the minor unit of `default_spend_request_currency`. + """ + + id: Optional[str] = None + """Stable Link user ID.""" + + email: Optional[str] = None + + first_name: Optional[str] = None + + last_name: Optional[str] = None + + name: Optional[str] = None + + phone: Optional[str] = None + + address: Optional[Address] = None + """The Link user's postal address. Omitted when Link reports none.""" + + agent_wallet_spend_limits: Optional[AgentWalletSpendLimits] = None + """Link's agent wallet spend limits, per transaction and over rolling periods.""" + + agent_wallet_step_up: Optional[AgentWalletStepUp] = None + """Verification Link requires from the user before the agent wallet can spend.""" + + default_spend_request_currency: Optional[str] = None + """Lowercase ISO currency Link applies to a spend request created without one.""" + + eligible_for_balance: Optional[bool] = None + """Whether the user can fund purchases from a Link balance.""" diff --git a/src/kernel/types/vaults/vault_item.py b/src/kernel/types/vaults/vault_item.py index 45d2f021..8064949b 100644 --- a/src/kernel/types/vaults/vault_item.py +++ b/src/kernel/types/vaults/vault_item.py @@ -6,11 +6,13 @@ from ..._utils import PropertyInfo from ..._models import BaseModel +from .link_user_info import LinkUserInfo from .vault_item_action import VaultItemAction from .card_vault_item_spec import CardVaultItemSpec from .vault_payment_method import VaultPaymentMethod from .card_vault_item_state import CardVaultItemState from .credential_vault_item import CredentialVaultItem +from .link_shipping_address import LinkShippingAddress from .wallet_vault_item_spec import WalletVaultItemSpec from .wallet_vault_item_state import WalletVaultItemState from .credential_account_vault_item import CredentialAccountVaultItem @@ -34,7 +36,7 @@ class WalletVaultItemAvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class WalletVaultItemAvailableOperation(BaseModel): @@ -64,6 +66,20 @@ class WalletVaultItemAvailableOperation(BaseModel): class WalletVaultItemExpanded(BaseModel): """Live, non-persisted data requested through the item GET expand parameter.""" + link_shipping_addresses: Optional[List[LinkShippingAddress]] = None + """ + Link's saved shipping addresses, present when the `link_shipping_addresses` + expansion is requested. + """ + + link_user_info: Optional[LinkUserInfo] = None + """ + Link's `GET /userinfo` response for the connected wallet, passed through as Link + reports it when the `link_user_info` expansion is requested. Kernel never stores + it. Null fields are null at Link; omitted fields were not reported. Amounts are + in the minor unit of `default_spend_request_currency`. + """ + payment_methods: Optional[List[VaultPaymentMethod]] = None @@ -122,7 +138,7 @@ class CardVaultItemAvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class CardVaultItemAvailableOperation(BaseModel): diff --git a/src/kernel/types/vaults/vault_item_operation_response.py b/src/kernel/types/vaults/vault_item_operation_response.py index a7a31733..03ec1cb7 100644 --- a/src/kernel/types/vaults/vault_item_operation_response.py +++ b/src/kernel/types/vaults/vault_item_operation_response.py @@ -5,11 +5,13 @@ from typing_extensions import Literal, TypeAlias from ..._models import BaseModel +from .link_user_info import LinkUserInfo from .vault_item_action import VaultItemAction from .card_vault_item_spec import CardVaultItemSpec from .vault_payment_method import VaultPaymentMethod from .card_vault_item_state import CardVaultItemState from .credential_vault_item import CredentialVaultItem +from .link_shipping_address import LinkShippingAddress from .wallet_vault_item_spec import WalletVaultItemSpec from .wallet_vault_item_state import WalletVaultItemState from .credential_account_vault_item import CredentialAccountVaultItem @@ -36,7 +38,7 @@ class WalletVaultItemAvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class WalletVaultItemAvailableOperation(BaseModel): @@ -66,6 +68,20 @@ class WalletVaultItemAvailableOperation(BaseModel): class WalletVaultItemExpanded(BaseModel): """Live, non-persisted data requested through the item GET expand parameter.""" + link_shipping_addresses: Optional[List[LinkShippingAddress]] = None + """ + Link's saved shipping addresses, present when the `link_shipping_addresses` + expansion is requested. + """ + + link_user_info: Optional[LinkUserInfo] = None + """ + Link's `GET /userinfo` response for the connected wallet, passed through as Link + reports it when the `link_user_info` expansion is requested. Kernel never stores + it. Null fields are null at Link; omitted fields were not reported. Amounts are + in the minor unit of `default_spend_request_currency`. + """ + payment_methods: Optional[List[VaultPaymentMethod]] = None @@ -124,7 +140,7 @@ class CardVaultItemAvailableExpansion(BaseModel): description: str - type: Literal["payment_methods"] + type: Literal["payment_methods", "link_user_info", "link_shipping_addresses"] class CardVaultItemAvailableOperation(BaseModel): diff --git a/tests/api_resources/vaults/test_items.py b/tests/api_resources/vaults/test_items.py index 96363302..9f891314 100644 --- a/tests/api_resources/vaults/test_items.py +++ b/tests/api_resources/vaults/test_items.py @@ -140,6 +140,10 @@ def test_method_update_with_all_params_overload_1(self, client: Kernel) -> None: } ], "metadata": {"foo": "string"}, + "recurring": { + "interval": "day", + "interval_count": 1, + }, "totals": [ { "amount": 0, @@ -1636,6 +1640,10 @@ def test_method_upsert_with_all_params_overload_2(self, client: Kernel) -> None: } ], "metadata": {"foo": "string"}, + "recurring": { + "interval": "day", + "interval_count": 1, + }, "totals": [ { "amount": 0, @@ -2077,6 +2085,10 @@ async def test_method_update_with_all_params_overload_1(self, async_client: Asyn } ], "metadata": {"foo": "string"}, + "recurring": { + "interval": "day", + "interval_count": 1, + }, "totals": [ { "amount": 0, @@ -3573,6 +3585,10 @@ async def test_method_upsert_with_all_params_overload_2(self, async_client: Asyn } ], "metadata": {"foo": "string"}, + "recurring": { + "interval": "day", + "interval_count": 1, + }, "totals": [ { "amount": 0, diff --git a/tests/test_pagination.py b/tests/test_pagination.py index cd59b3c6..99276064 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -3,6 +3,8 @@ import httpx import pytest +from kernel import Kernel, AsyncKernel +from kernel._models import FinalRequestOptions from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination PageClass = Union[Type[SyncOffsetPagination[Any]], Type[AsyncOffsetPagination[Any]]] @@ -12,14 +14,31 @@ both_classes = pytest.mark.parametrize("cls", [SyncOffsetPagination, AsyncOffsetPagination]) -def _page(cls: PageClass, *, items: List[Any], next_offset: Optional[int], has_more: Optional[bool]) -> Any: +def client_for(cls: PageClass) -> Any: + if cls is SyncOffsetPagination: + return Kernel(api_key="test-api-key", base_url="http://127.0.0.1") + return AsyncKernel(api_key="test-api-key", base_url="http://127.0.0.1") + + +def _page( + cls: PageClass, + *, + items: List[Any], + next_offset: Optional[int], + has_more: Optional[bool], + request_offset: Optional[int] = None, +) -> Any: headers: dict[str, str] = {} if next_offset is not None: headers["X-Next-Offset"] = str(next_offset) if has_more is not None: headers["X-Has-More"] = "true" if has_more else "false" response = httpx.Response(200, headers=headers) - return cls.build(response=response, data=items) + page = cls.build(response=response, data=items) + params = {} if request_offset is None else {"offset": request_offset} + options = FinalRequestOptions(method="get", url="/proxies", params=params) + page._set_private_attributes(client=client_for(cls), model=object, options=options) # type: ignore[arg-type] + return page @both_classes @@ -54,8 +73,40 @@ def test_stops_on_next_offset_zero_sentinel(cls: PageClass) -> None: @both_classes -def test_stops_on_empty_page(cls: PageClass) -> None: - page = _page(cls, items=[], next_offset=300, has_more=True) +def test_continues_past_empty_page_while_x_has_more_true(cls: PageClass) -> None: + # A server-side filter can leave a page empty with more records ahead. + page = _page(cls, items=[], next_offset=300, has_more=True, request_offset=200) + assert page.has_next_page() is True + info = page.next_page_info() + assert info is not None + assert info.params == {"offset": 300} + + +@both_classes +def test_stops_on_empty_page_when_x_has_more_false(cls: PageClass) -> None: + page = _page(cls, items=[], next_offset=0, has_more=False, request_offset=200) + assert page.has_next_page() is False + + +@both_classes +def test_stops_when_next_offset_repeats_requested_offset(cls: PageClass) -> None: + page = _page(cls, items=[], next_offset=200, has_more=True, request_offset=200) + assert page.next_page_info() is None + assert page.has_next_page() is False + + +@both_classes +def test_continues_past_empty_page_with_descending_cursor(cls: PageClass) -> None: + page = _page(cls, items=[], next_offset=200, has_more=True, request_offset=300) + assert page.has_next_page() is True + info = page.next_page_info() + assert info is not None + assert info.params == {"offset": 200} + + +@both_classes +def test_stops_on_empty_first_page_without_pagination_headers(cls: PageClass) -> None: + page = _page(cls, items=[], next_offset=None, has_more=None) assert page.has_next_page() is False