diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a26ebfc..8f3e0a4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.0" + ".": "0.15.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 0976618..69b13d9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 35 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-1949bcfc8775c97eca880428dc93e9f97aa91144bef82584027ede5089bb2e19.yml -openapi_spec_hash: 0aa367455a067b701f18ef7892b6c7e9 -config_hash: 373e654f8034a40c42234eee9ebefbb9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-39b91ffd46b6e41924f8465ffaaff6ba3c200a68daa513d4f1eb1e4b29aba78f.yml +openapi_spec_hash: 542dd50007316698c83e8b0bdd5e40e2 +config_hash: 77a3908ee910a8019f5831d3a3d53c18 diff --git a/CHANGELOG.md b/CHANGELOG.md index aabac85..33dca82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.15.0 (2026-01-30) + +Full Changelog: [v0.14.0...v0.15.0](https://github.com/ArkHQ-io/ark-python/compare/v0.14.0...v0.15.0) + +### Features + +* **api:** api update ([0f5c166](https://github.com/ArkHQ-io/ark-python/commit/0f5c1666d7c02d1f18096610716be6cb2c39a281)) +* **api:** api update ([f128894](https://github.com/ArkHQ-io/ark-python/commit/f128894ef84fffa757424b4b1684f7e4eebf4629)) +* **api:** manual updates ([bcc7230](https://github.com/ArkHQ-io/ark-python/commit/bcc72308bfbfd6bfd6cefc6df9e1019637ce1b02)) +* **api:** manual updates ([378cd65](https://github.com/ArkHQ-io/ark-python/commit/378cd65aadfd41edd36f4d82ad3ea12d95ec8f0c)) +* **api:** manual updates ([edb503c](https://github.com/ArkHQ-io/ark-python/commit/edb503c58439ac4face4260cf1e6794cab79f8bb)) +* **client:** add custom JSON encoder for extended type support ([ff51eb2](https://github.com/ArkHQ-io/ark-python/commit/ff51eb229a2eda74e3bc8c16d5f7f43758dc5c31)) + ## 0.14.0 (2026-01-29) Full Changelog: [v0.13.0...v0.14.0](https://github.com/ArkHQ-io/ark-python/compare/v0.13.0...v0.14.0) diff --git a/api.md b/api.md index d511272..7baaad3 100644 --- a/api.md +++ b/api.md @@ -22,10 +22,10 @@ from ark.types import ( Methods: -- client.emails.retrieve(email_id, \*\*params) -> EmailRetrieveResponse +- client.emails.retrieve(id, \*\*params) -> EmailRetrieveResponse - client.emails.list(\*\*params) -> SyncPageNumberPagination[EmailListResponse] -- client.emails.retrieve_deliveries(email_id) -> EmailRetrieveDeliveriesResponse -- client.emails.retry(email_id) -> EmailRetryResponse +- client.emails.retrieve_deliveries(id) -> EmailRetrieveDeliveriesResponse +- client.emails.retry(id) -> EmailRetryResponse - client.emails.send(\*\*params) -> EmailSendResponse - client.emails.send_batch(\*\*params) -> EmailSendBatchResponse - client.emails.send_raw(\*\*params) -> EmailSendRawResponse diff --git a/pyproject.toml b/pyproject.toml index 0f30e15..6c28b5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ark-email" -version = "0.14.0" +version = "0.15.0" description = "The official Python library for the ark API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/ark/_base_client.py b/src/ark/_base_client.py index e3e6436..6fd69b9 100644 --- a/src/ark/_base_client.py +++ b/src/ark/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/ark/_compat.py b/src/ark/_compat.py index bdef67f..786ff42 100644 --- a/src/ark/_compat.py +++ b/src/ark/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/ark/_utils/_json.py b/src/ark/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/ark/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/src/ark/_version.py b/src/ark/_version.py index 9e3345c..1750a65 100644 --- a/src/ark/_version.py +++ b/src/ark/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "ark" -__version__ = "0.14.0" # x-release-please-version +__version__ = "0.15.0" # x-release-please-version diff --git a/src/ark/resources/emails.py b/src/ark/resources/emails.py index cd1f533..4169d3a 100644 --- a/src/ark/resources/emails.py +++ b/src/ark/resources/emails.py @@ -59,7 +59,7 @@ def with_streaming_response(self) -> EmailsResourceWithStreamingResponse: def retrieve( self, - email_id: str, + id: str, *, expand: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -96,10 +96,10 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/emails/{email_id}", + f"/emails/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -200,7 +200,7 @@ def list( def retrieve_deliveries( self, - email_id: str, + 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. @@ -243,8 +243,8 @@ def retrieve_deliveries( ### Can Retry Manually - Indicates whether you can call `POST /emails/{emailId}/retry` to manually retry - the email. This is `true` when the raw message content is still available (not + Indicates whether you can call `POST /emails/{id}/retry` to manually retry the + email. This is `true` when the raw message content is still available (not expired due to retention policy). Args: @@ -256,10 +256,10 @@ def retrieve_deliveries( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - f"/emails/{email_id}/deliveries", + f"/emails/{id}/deliveries", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -268,7 +268,7 @@ def retrieve_deliveries( def retry( self, - email_id: str, + 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. @@ -293,10 +293,10 @@ def retry( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._post( - f"/emails/{email_id}/retry", + f"/emails/{id}/retry", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -575,7 +575,7 @@ def with_streaming_response(self) -> AsyncEmailsResourceWithStreamingResponse: async def retrieve( self, - email_id: str, + id: str, *, expand: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -612,10 +612,10 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/emails/{email_id}", + f"/emails/{id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -716,7 +716,7 @@ def list( async def retrieve_deliveries( self, - email_id: str, + 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. @@ -759,8 +759,8 @@ async def retrieve_deliveries( ### Can Retry Manually - Indicates whether you can call `POST /emails/{emailId}/retry` to manually retry - the email. This is `true` when the raw message content is still available (not + Indicates whether you can call `POST /emails/{id}/retry` to manually retry the + email. This is `true` when the raw message content is still available (not expired due to retention policy). Args: @@ -772,10 +772,10 @@ async def retrieve_deliveries( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - f"/emails/{email_id}/deliveries", + f"/emails/{id}/deliveries", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -784,7 +784,7 @@ async def retrieve_deliveries( async def retry( self, - email_id: str, + 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. @@ -809,10 +809,10 @@ async def retry( timeout: Override the client-level default timeout for this request, in seconds """ - if not email_id: - raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}") + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._post( - f"/emails/{email_id}/retry", + f"/emails/{id}/retry", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/ark/types/email_list_response.py b/src/ark/types/email_list_response.py index f8a1d1c..dce7d39 100644 --- a/src/ark/types/email_list_response.py +++ b/src/ark/types/email_list_response.py @@ -13,9 +13,7 @@ class EmailListResponse(BaseModel): id: str - """Internal message ID""" - - token: str + """Unique message identifier (token)""" from_: str = FieldInfo(alias="from") diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py index 584bad7..6406e9e 100644 --- a/src/ark/types/email_retrieve_deliveries_response.py +++ b/src/ark/types/email_retrieve_deliveries_response.py @@ -88,9 +88,12 @@ class DataRetryState(BaseModel): class Data(BaseModel): + id: str + """Message identifier (token)""" + can_retry_manually: bool = FieldInfo(alias="canRetryManually") """ - Whether the message can be manually retried via `POST /emails/{emailId}/retry`. + Whether the message can be manually retried via `POST /emails/{id}/retry`. `true` when the raw message content is still available (not expired). Messages older than the retention period cannot be retried. """ @@ -101,12 +104,6 @@ class Data(BaseModel): SMTP response codes and timestamps. """ - message_id: int = FieldInfo(alias="messageId") - """Internal numeric message ID""" - - message_token: str = FieldInfo(alias="messageToken") - """Unique message token for API references""" - retry_state: Optional[DataRetryState] = FieldInfo(alias="retryState", default=None) """ Information about the current retry state of a message that is queued for diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py index b250031..f60801e 100644 --- a/src/ark/types/email_retrieve_response.py +++ b/src/ark/types/email_retrieve_response.py @@ -108,14 +108,7 @@ class DataDelivery(BaseModel): class Data(BaseModel): id: str - """Internal message ID""" - - token: str - """ - Unique message token used to retrieve this email via API. Combined with id to - form the full message identifier: msg*{id}*{token} Use this token with GET - /emails/{emailId} where emailId = "msg*{id}*{token}" - """ + """Unique message identifier (token)""" from_: str = FieldInfo(alias="from") """Sender address""" diff --git a/src/ark/types/email_retry_response.py b/src/ark/types/email_retry_response.py index 8c5029b..f63cc99 100644 --- a/src/ark/types/email_retry_response.py +++ b/src/ark/types/email_retry_response.py @@ -9,6 +9,9 @@ class Data(BaseModel): + id: str + """Email identifier (token)""" + message: str diff --git a/src/ark/types/email_send_batch_response.py b/src/ark/types/email_send_batch_response.py index 272119c..d32e3fc 100644 --- a/src/ark/types/email_send_batch_response.py +++ b/src/ark/types/email_send_batch_response.py @@ -11,9 +11,7 @@ class DataMessages(BaseModel): id: str - """Message ID""" - - token: str + """Message identifier (token)""" class Data(BaseModel): diff --git a/src/ark/types/email_send_raw_response.py b/src/ark/types/email_send_raw_response.py index f1e8c03..46e7c96 100644 --- a/src/ark/types/email_send_raw_response.py +++ b/src/ark/types/email_send_raw_response.py @@ -13,7 +13,7 @@ class Data(BaseModel): id: str - """Unique message ID (format: msg*{id}*{token})""" + """Unique message identifier (token)""" status: Literal["pending", "sent"] """Current delivery status""" diff --git a/src/ark/types/email_send_response.py b/src/ark/types/email_send_response.py index 39e1d36..cb7d814 100644 --- a/src/ark/types/email_send_response.py +++ b/src/ark/types/email_send_response.py @@ -13,7 +13,7 @@ class Data(BaseModel): id: str - """Unique message ID (format: msg*{id}*{token})""" + """Unique message identifier (token)""" status: Literal["pending", "sent"] """Current delivery status""" diff --git a/src/ark/types/log_entry.py b/src/ark/types/log_entry.py index 9fad7e7..4dfdd6c 100644 --- a/src/ark/types/log_entry.py +++ b/src/ark/types/log_entry.py @@ -57,7 +57,7 @@ class Email(BaseModel): """Email-specific data (for email endpoints)""" id: Optional[str] = None - """Email message ID""" + """Email message identifier (token)""" recipient_count: Optional[int] = FieldInfo(alias="recipientCount", default=None) """Number of recipients""" diff --git a/tests/api_resources/test_emails.py b/tests/api_resources/test_emails.py index 17781ac..7a54df7 100644 --- a/tests/api_resources/test_emails.py +++ b/tests/api_resources/test_emails.py @@ -29,14 +29,14 @@ class TestEmails: @parametrize def test_method_retrieve(self, client: Ark) -> None: email = client.emails.retrieve( - email_id="emailId", + id="aBc123XyZ", ) assert_matches_type(EmailRetrieveResponse, email, path=["response"]) @parametrize def test_method_retrieve_with_all_params(self, client: Ark) -> None: email = client.emails.retrieve( - email_id="emailId", + id="aBc123XyZ", expand="full", ) assert_matches_type(EmailRetrieveResponse, email, path=["response"]) @@ -44,7 +44,7 @@ def test_method_retrieve_with_all_params(self, client: Ark) -> None: @parametrize def test_raw_response_retrieve(self, client: Ark) -> None: response = client.emails.with_raw_response.retrieve( - email_id="emailId", + id="aBc123XyZ", ) assert response.is_closed is True @@ -55,7 +55,7 @@ def test_raw_response_retrieve(self, client: Ark) -> None: @parametrize def test_streaming_response_retrieve(self, client: Ark) -> None: with client.emails.with_streaming_response.retrieve( - email_id="emailId", + id="aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -67,9 +67,9 @@ def test_streaming_response_retrieve(self, client: Ark) -> None: @parametrize def test_path_params_retrieve(self, client: Ark) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.emails.with_raw_response.retrieve( - email_id="", + id="", ) @parametrize @@ -114,14 +114,14 @@ def test_streaming_response_list(self, client: Ark) -> None: @parametrize def test_method_retrieve_deliveries(self, client: Ark) -> None: email = client.emails.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"]) @parametrize def test_raw_response_retrieve_deliveries(self, client: Ark) -> None: response = client.emails.with_raw_response.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_retrieve_deliveries(self, client: Ark) -> None: @parametrize def test_streaming_response_retrieve_deliveries(self, client: Ark) -> None: with client.emails.with_streaming_response.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -144,7 +144,7 @@ def test_streaming_response_retrieve_deliveries(self, client: Ark) -> None: @parametrize def test_path_params_retrieve_deliveries(self, client: Ark) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.emails.with_raw_response.retrieve_deliveries( "", ) @@ -152,14 +152,14 @@ def test_path_params_retrieve_deliveries(self, client: Ark) -> None: @parametrize def test_method_retry(self, client: Ark) -> None: email = client.emails.retry( - "emailId", + "aBc123XyZ", ) assert_matches_type(EmailRetryResponse, email, path=["response"]) @parametrize def test_raw_response_retry(self, client: Ark) -> None: response = client.emails.with_raw_response.retry( - "emailId", + "aBc123XyZ", ) assert response.is_closed is True @@ -170,7 +170,7 @@ def test_raw_response_retry(self, client: Ark) -> None: @parametrize def test_streaming_response_retry(self, client: Ark) -> None: with client.emails.with_streaming_response.retry( - "emailId", + "aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -182,7 +182,7 @@ def test_streaming_response_retry(self, client: Ark) -> None: @parametrize def test_path_params_retry(self, client: Ark) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.emails.with_raw_response.retry( "", ) @@ -403,14 +403,14 @@ class TestAsyncEmails: @parametrize async def test_method_retrieve(self, async_client: AsyncArk) -> None: email = await async_client.emails.retrieve( - email_id="emailId", + id="aBc123XyZ", ) assert_matches_type(EmailRetrieveResponse, email, path=["response"]) @parametrize async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None: email = await async_client.emails.retrieve( - email_id="emailId", + id="aBc123XyZ", expand="full", ) assert_matches_type(EmailRetrieveResponse, email, path=["response"]) @@ -418,7 +418,7 @@ async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> @parametrize async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None: response = await async_client.emails.with_raw_response.retrieve( - email_id="emailId", + id="aBc123XyZ", ) assert response.is_closed is True @@ -429,7 +429,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None: async with async_client.emails.with_streaming_response.retrieve( - email_id="emailId", + id="aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -441,9 +441,9 @@ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None @parametrize async def test_path_params_retrieve(self, async_client: AsyncArk) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.emails.with_raw_response.retrieve( - email_id="", + id="", ) @parametrize @@ -488,14 +488,14 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None: @parametrize async def test_method_retrieve_deliveries(self, async_client: AsyncArk) -> None: email = await async_client.emails.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) assert_matches_type(EmailRetrieveDeliveriesResponse, email, path=["response"]) @parametrize async def test_raw_response_retrieve_deliveries(self, async_client: AsyncArk) -> None: response = await async_client.emails.with_raw_response.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) assert response.is_closed is True @@ -506,7 +506,7 @@ async def test_raw_response_retrieve_deliveries(self, async_client: AsyncArk) -> @parametrize async def test_streaming_response_retrieve_deliveries(self, async_client: AsyncArk) -> None: async with async_client.emails.with_streaming_response.retrieve_deliveries( - "msg_12345_aBc123XyZ", + "aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -518,7 +518,7 @@ async def test_streaming_response_retrieve_deliveries(self, async_client: AsyncA @parametrize async def test_path_params_retrieve_deliveries(self, async_client: AsyncArk) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.emails.with_raw_response.retrieve_deliveries( "", ) @@ -526,14 +526,14 @@ async def test_path_params_retrieve_deliveries(self, async_client: AsyncArk) -> @parametrize async def test_method_retry(self, async_client: AsyncArk) -> None: email = await async_client.emails.retry( - "emailId", + "aBc123XyZ", ) assert_matches_type(EmailRetryResponse, email, path=["response"]) @parametrize async def test_raw_response_retry(self, async_client: AsyncArk) -> None: response = await async_client.emails.with_raw_response.retry( - "emailId", + "aBc123XyZ", ) assert response.is_closed is True @@ -544,7 +544,7 @@ async def test_raw_response_retry(self, async_client: AsyncArk) -> None: @parametrize async def test_streaming_response_retry(self, async_client: AsyncArk) -> None: async with async_client.emails.with_streaming_response.retry( - "emailId", + "aBc123XyZ", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -556,7 +556,7 @@ async def test_streaming_response_retry(self, async_client: AsyncArk) -> None: @parametrize async def test_path_params_retry(self, async_client: AsyncArk) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.emails.with_raw_response.retry( "", ) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..7837132 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from ark import _compat +from ark._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'