diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cff01f26..b2585653 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.15.0" + ".": "2.16.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 3114caa3..e670c774 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ -configured_endpoints: 96 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-1345a8e288e34d5477b0e189877225f83939a59078c22fbb5367712e376c5d19.yml +configured_endpoints: 97 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-bf3e71b33372f4a9307f4b6cb689ea46b3cf583ecc5d79eee9601cd0b0467c9a.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index c74492bf..0efd74be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 2.16.0 (2024-11-15) + +Full Changelog: [v2.15.0...v2.16.0](https://github.com/orbcorp/orb-python/compare/v2.15.0...v2.16.0) + +### Features + +* **api:** api update ([#430](https://github.com/orbcorp/orb-python/issues/430)) ([5457ac2](https://github.com/orbcorp/orb-python/commit/5457ac24adc55caee65382214e0cbf9739e963f3)) + + +### Chores + +* rebuild project due to codegen change ([#427](https://github.com/orbcorp/orb-python/issues/427)) ([bf7c315](https://github.com/orbcorp/orb-python/commit/bf7c315a84c6b959369bfc1f255f9353ad5c3c6f)) +* rebuild project due to codegen change ([#429](https://github.com/orbcorp/orb-python/issues/429)) ([2bfa0a2](https://github.com/orbcorp/orb-python/commit/2bfa0a206c28724761471ed98d5c619d33a2506b)) + ## 2.15.0 (2024-11-06) Full Changelog: [v2.14.0...v2.15.0](https://github.com/orbcorp/orb-python/compare/v2.14.0...v2.15.0) diff --git a/README.md b/README.md index 3936ac2c..1bc81f80 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ import os from orb import Orb client = Orb( - # This is the default and can be omitted - api_key=os.environ.get("ORB_API_KEY"), + api_key=os.environ.get("ORB_API_KEY"), # This is the default and can be omitted ) customer = client.customers.create( @@ -52,8 +51,7 @@ import asyncio from orb import AsyncOrb client = AsyncOrb( - # This is the default and can be omitted - api_key=os.environ.get("ORB_API_KEY"), + api_key=os.environ.get("ORB_API_KEY"), # This is the default and can be omitted ) diff --git a/api.md b/api.md index c441ce56..7d1e5f7a 100644 --- a/api.md +++ b/api.md @@ -220,6 +220,7 @@ Methods: - client.invoices.fetch_upcoming(\*\*params) -> InvoiceFetchUpcomingResponse - client.invoices.issue(invoice_id) -> Invoice - client.invoices.mark_paid(invoice_id, \*\*params) -> Invoice +- client.invoices.pay(invoice_id) -> Invoice - client.invoices.void(invoice_id) -> Invoice # Items diff --git a/pyproject.toml b/pyproject.toml index 527302b0..43bff382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "orb-billing" -version = "2.15.0" +version = "2.16.0" description = "The official Python library for the orb API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/orb/_utils/_transform.py b/src/orb/_utils/_transform.py index 47e262a5..71f35299 100644 --- a/src/orb/_utils/_transform.py +++ b/src/orb/_utils/_transform.py @@ -311,6 +311,11 @@ async def _async_transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + inner_type = extract_type_arg(stripped_type, 0) return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] diff --git a/src/orb/_version.py b/src/orb/_version.py index 95fab6f1..5826d194 100644 --- a/src/orb/_version.py +++ b/src/orb/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "orb" -__version__ = "2.15.0" # x-release-please-version +__version__ = "2.16.0" # x-release-please-version diff --git a/src/orb/resources/invoices.py b/src/orb/resources/invoices.py index f522e06f..6c40a39f 100644 --- a/src/orb/resources/invoices.py +++ b/src/orb/resources/invoices.py @@ -451,6 +451,47 @@ def mark_paid( cast_to=Invoice, ) + def pay( + self, + invoice_id: 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, + idempotency_key: str | None = None, + ) -> Invoice: + """ + This endpoint collects payment for an invoice using the customer's default + payment method. This action can only be taken on invoices with status "issued". + + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not invoice_id: + raise ValueError(f"Expected a non-empty value for `invoice_id` but received {invoice_id!r}") + return self._post( + f"/invoices/{invoice_id}/pay", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Invoice, + ) + def void( self, invoice_id: str, @@ -917,6 +958,47 @@ async def mark_paid( cast_to=Invoice, ) + async def pay( + self, + invoice_id: 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, + idempotency_key: str | None = None, + ) -> Invoice: + """ + This endpoint collects payment for an invoice using the customer's default + payment method. This action can only be taken on invoices with status "issued". + + 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 + + idempotency_key: Specify a custom idempotency key for this request + """ + if not invoice_id: + raise ValueError(f"Expected a non-empty value for `invoice_id` but received {invoice_id!r}") + return await self._post( + f"/invoices/{invoice_id}/pay", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=Invoice, + ) + async def void( self, invoice_id: str, @@ -990,6 +1072,9 @@ def __init__(self, invoices: Invoices) -> None: self.mark_paid = _legacy_response.to_raw_response_wrapper( invoices.mark_paid, ) + self.pay = _legacy_response.to_raw_response_wrapper( + invoices.pay, + ) self.void = _legacy_response.to_raw_response_wrapper( invoices.void, ) @@ -1020,6 +1105,9 @@ def __init__(self, invoices: AsyncInvoices) -> None: self.mark_paid = _legacy_response.async_to_raw_response_wrapper( invoices.mark_paid, ) + self.pay = _legacy_response.async_to_raw_response_wrapper( + invoices.pay, + ) self.void = _legacy_response.async_to_raw_response_wrapper( invoices.void, ) @@ -1050,6 +1138,9 @@ def __init__(self, invoices: Invoices) -> None: self.mark_paid = to_streamed_response_wrapper( invoices.mark_paid, ) + self.pay = to_streamed_response_wrapper( + invoices.pay, + ) self.void = to_streamed_response_wrapper( invoices.void, ) @@ -1080,6 +1171,9 @@ def __init__(self, invoices: AsyncInvoices) -> None: self.mark_paid = async_to_streamed_response_wrapper( invoices.mark_paid, ) + self.pay = async_to_streamed_response_wrapper( + invoices.pay, + ) self.void = async_to_streamed_response_wrapper( invoices.void, ) diff --git a/src/orb/types/invoice.py b/src/orb/types/invoice.py index 5781f30d..4019c548 100644 --- a/src/orb/types/invoice.py +++ b/src/orb/types/invoice.py @@ -34,6 +34,7 @@ "LineItemTaxAmount", "Maximum", "Minimum", + "PaymentAttempt", "ShippingAddress", "Subscription", ] @@ -743,6 +744,26 @@ class Minimum(BaseModel): """Minimum amount applied""" +class PaymentAttempt(BaseModel): + id: str + """The ID of the payment attempt.""" + + amount: str + """The amount of the payment attempt.""" + + created_at: datetime + """The time at which the payment attempt was created.""" + + payment_provider: Optional[Literal["stripe"]] = None + """The payment provider that attempted to collect the payment.""" + + payment_provider_id: Optional[str] = None + """The ID of the payment attempt in the payment provider.""" + + succeeded: bool + """Whether the payment attempt succeeded.""" + + class ShippingAddress(BaseModel): city: Optional[str] = None @@ -970,6 +991,9 @@ class Invoice(BaseModel): was paid. """ + payment_attempts: List[PaymentAttempt] + """A list of payment attempts associated with the invoice""" + payment_failed_at: Optional[datetime] = None """ If payment was attempted on this invoice but failed, this will be the time of diff --git a/src/orb/types/invoice_fetch_upcoming_response.py b/src/orb/types/invoice_fetch_upcoming_response.py index f1398c16..f59a6eda 100644 --- a/src/orb/types/invoice_fetch_upcoming_response.py +++ b/src/orb/types/invoice_fetch_upcoming_response.py @@ -34,6 +34,7 @@ "LineItemTaxAmount", "Maximum", "Minimum", + "PaymentAttempt", "ShippingAddress", "Subscription", ] @@ -743,6 +744,26 @@ class Minimum(BaseModel): """Minimum amount applied""" +class PaymentAttempt(BaseModel): + id: str + """The ID of the payment attempt.""" + + amount: str + """The amount of the payment attempt.""" + + created_at: datetime + """The time at which the payment attempt was created.""" + + payment_provider: Optional[Literal["stripe"]] = None + """The payment provider that attempted to collect the payment.""" + + payment_provider_id: Optional[str] = None + """The ID of the payment attempt in the payment provider.""" + + succeeded: bool + """Whether the payment attempt succeeded.""" + + class ShippingAddress(BaseModel): city: Optional[str] = None @@ -967,6 +988,9 @@ class InvoiceFetchUpcomingResponse(BaseModel): was paid. """ + payment_attempts: List[PaymentAttempt] + """A list of payment attempts associated with the invoice""" + payment_failed_at: Optional[datetime] = None """ If payment was attempted on this invoice but failed, this will be the time of diff --git a/tests/api_resources/test_invoices.py b/tests/api_resources/test_invoices.py index 16fd1453..7121b809 100644 --- a/tests/api_resources/test_invoices.py +++ b/tests/api_resources/test_invoices.py @@ -379,6 +379,44 @@ def test_path_params_mark_paid(self, client: Orb) -> None: payment_received_date=parse_date("2023-09-22"), ) + @parametrize + def test_method_pay(self, client: Orb) -> None: + invoice = client.invoices.pay( + "invoice_id", + ) + assert_matches_type(Invoice, invoice, path=["response"]) + + @parametrize + def test_raw_response_pay(self, client: Orb) -> None: + response = client.invoices.with_raw_response.pay( + "invoice_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invoice = response.parse() + assert_matches_type(Invoice, invoice, path=["response"]) + + @parametrize + def test_streaming_response_pay(self, client: Orb) -> None: + with client.invoices.with_streaming_response.pay( + "invoice_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invoice = response.parse() + assert_matches_type(Invoice, invoice, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_pay(self, client: Orb) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invoice_id` but received ''"): + client.invoices.with_raw_response.pay( + "", + ) + @parametrize def test_method_void(self, client: Orb) -> None: invoice = client.invoices.void( @@ -778,6 +816,44 @@ async def test_path_params_mark_paid(self, async_client: AsyncOrb) -> None: payment_received_date=parse_date("2023-09-22"), ) + @parametrize + async def test_method_pay(self, async_client: AsyncOrb) -> None: + invoice = await async_client.invoices.pay( + "invoice_id", + ) + assert_matches_type(Invoice, invoice, path=["response"]) + + @parametrize + async def test_raw_response_pay(self, async_client: AsyncOrb) -> None: + response = await async_client.invoices.with_raw_response.pay( + "invoice_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + invoice = response.parse() + assert_matches_type(Invoice, invoice, path=["response"]) + + @parametrize + async def test_streaming_response_pay(self, async_client: AsyncOrb) -> None: + async with async_client.invoices.with_streaming_response.pay( + "invoice_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + invoice = await response.parse() + assert_matches_type(Invoice, invoice, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_pay(self, async_client: AsyncOrb) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invoice_id` but received ''"): + await async_client.invoices.with_raw_response.pay( + "", + ) + @parametrize async def test_method_void(self, async_client: AsyncOrb) -> None: invoice = await async_client.invoices.void(