diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1964062..0fbfd05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Install dependencies run: uv sync --all-extras @@ -46,7 +46,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Install dependencies run: uv sync --all-extras @@ -80,7 +80,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d661066..e756293 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.18.1" + ".": "0.19.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index c8ee129..e530c9b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 58 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-98a90852ffca49f4e26c613afff433b17023ee1f81f38ad38a5dad60a0d09881.yml -openapi_spec_hash: c6fd865dd6995df15cf9e6ada2ae791e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-06c3025bf12b191c3906b28173c9b359e24481dd2839dbf3e6dd0b80c1de3fd6.yml +openapi_spec_hash: d8f8fb1f78579997b6381d64cba4e826 config_hash: b70b11b10fc614f91f1c6f028b40780f diff --git a/CHANGELOG.md b/CHANGELOG.md index a755aaa..eaca31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.19.0 (2026-02-27) + +Full Changelog: [v0.18.1...v0.19.0](https://github.com/ArkHQ-io/ark-python/compare/v0.18.1...v0.19.0) + +### Features + +* **api:** add tenantId to send ([3eddd67](https://github.com/ArkHQ-io/ark-python/commit/3eddd677b69f387149336e11abe71a6143290ac4)) + + +### Chores + +* **ci:** bump uv version ([e7115ed](https://github.com/ArkHQ-io/ark-python/commit/e7115edad4dd96f45c7b87f76b00792b8d096647)) +* **internal:** add request options to SSE classes ([fdc5e91](https://github.com/ArkHQ-io/ark-python/commit/fdc5e91d4774006a051e8a289bbd1b3c7eec1b8c)) +* **internal:** make `test_proxy_environment_variables` more resilient ([709aff4](https://github.com/ArkHQ-io/ark-python/commit/709aff401224092c3e5059559951c8bc82c59866)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([df5b863](https://github.com/ArkHQ-io/ark-python/commit/df5b8639e08e14c7a64e51081f60c41d0450617b)) +* update mock server docs ([b4e4ce8](https://github.com/ArkHQ-io/ark-python/commit/b4e4ce8a56859a87137349e5f97ede2c8acaad25)) + ## 0.18.1 (2026-02-18) Full Changelog: [v0.18.0...v0.18.1](https://github.com/ArkHQ-io/ark-python/compare/v0.18.0...v0.18.1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a02fdd..e40b567 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. ```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh diff --git a/pyproject.toml b/pyproject.toml index 0adad94..d762cba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ark-email" -version = "0.18.1" +version = "0.19.0" description = "The official Python library for the ark API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/ark/_response.py b/src/ark/_response.py index 5fd3b90..6125f53 100644 --- a/src/ark/_response.py +++ b/src/ark/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/ark/_streaming.py b/src/ark/_streaming.py index a271a69..cd236ca 100644 --- a/src/ark/_streaming.py +++ b/src/ark/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Ark, AsyncArk + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Ark, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncArk, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() diff --git a/src/ark/_version.py b/src/ark/_version.py index 39f8f2d..a626377 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.18.1" # x-release-please-version +__version__ = "0.19.0" # x-release-please-version diff --git a/src/ark/resources/emails.py b/src/ark/resources/emails.py index 77631ce..508b5e9 100644 --- a/src/ark/resources/emails.py +++ b/src/ark/resources/emails.py @@ -317,6 +317,7 @@ def send( metadata: Optional[Dict[str, str]] | Omit = omit, reply_to: Optional[str] | Omit = omit, tag: Optional[str] | Omit = omit, + tenant_id: Optional[str] | Omit = omit, text: Optional[str] | Omit = omit, idempotency_key: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -396,6 +397,14 @@ def send( tag: Tag for categorization and filtering (accepts null) + tenant_id: The tenant ID to send this email from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + text: Plain text body (accepts null, auto-generated from HTML if not provided). Maximum 5MB (5,242,880 characters). @@ -423,6 +432,7 @@ def send( "metadata": metadata, "reply_to": reply_to, "tag": tag, + "tenant_id": tenant_id, "text": text, }, email_send_params.EmailSendParams, @@ -438,6 +448,7 @@ def send_batch( *, emails: Iterable[email_send_batch_params.Email], from_: str, + tenant_id: Optional[str] | Omit = omit, idempotency_key: str | 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. @@ -459,6 +470,14 @@ def send_batch( Args: from_: Sender email for all messages + tenant_id: The tenant ID to send this batch from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -474,6 +493,7 @@ def send_batch( { "emails": emails, "from_": from_, + "tenant_id": tenant_id, }, email_send_batch_params.EmailSendBatchParams, ), @@ -490,6 +510,7 @@ def send_raw( raw_message: str, to: SequenceNotStr[str], bounce: Optional[bool] | Omit = omit, + tenant_id: Optional[str] | 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. extra_headers: Headers | None = None, @@ -527,6 +548,14 @@ def send_raw( bounce: Whether this is a bounce message (accepts null) + tenant_id: The tenant ID to send this email from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -543,6 +572,7 @@ def send_raw( "raw_message": raw_message, "to": to, "bounce": bounce, + "tenant_id": tenant_id, }, email_send_raw_params.EmailSendRawParams, ), @@ -833,6 +863,7 @@ async def send( metadata: Optional[Dict[str, str]] | Omit = omit, reply_to: Optional[str] | Omit = omit, tag: Optional[str] | Omit = omit, + tenant_id: Optional[str] | Omit = omit, text: Optional[str] | Omit = omit, idempotency_key: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -912,6 +943,14 @@ async def send( tag: Tag for categorization and filtering (accepts null) + tenant_id: The tenant ID to send this email from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + text: Plain text body (accepts null, auto-generated from HTML if not provided). Maximum 5MB (5,242,880 characters). @@ -939,6 +978,7 @@ async def send( "metadata": metadata, "reply_to": reply_to, "tag": tag, + "tenant_id": tenant_id, "text": text, }, email_send_params.EmailSendParams, @@ -954,6 +994,7 @@ async def send_batch( *, emails: Iterable[email_send_batch_params.Email], from_: str, + tenant_id: Optional[str] | Omit = omit, idempotency_key: str | 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. @@ -975,6 +1016,14 @@ async def send_batch( Args: from_: Sender email for all messages + tenant_id: The tenant ID to send this batch from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -990,6 +1039,7 @@ async def send_batch( { "emails": emails, "from_": from_, + "tenant_id": tenant_id, }, email_send_batch_params.EmailSendBatchParams, ), @@ -1006,6 +1056,7 @@ async def send_raw( raw_message: str, to: SequenceNotStr[str], bounce: Optional[bool] | Omit = omit, + tenant_id: Optional[str] | 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. extra_headers: Headers | None = None, @@ -1043,6 +1094,14 @@ async def send_raw( bounce: Whether this is a bounce message (accepts null) + tenant_id: The tenant ID to send this email from. Determines which tenant's configuration + (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -1059,6 +1118,7 @@ async def send_raw( "raw_message": raw_message, "to": to, "bounce": bounce, + "tenant_id": tenant_id, }, email_send_raw_params.EmailSendRawParams, ), diff --git a/src/ark/types/email_list_response.py b/src/ark/types/email_list_response.py index dce7d39..21bd18e 100644 --- a/src/ark/types/email_list_response.py +++ b/src/ark/types/email_list_response.py @@ -30,6 +30,9 @@ class EmailListResponse(BaseModel): subject: str + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email belongs to""" + timestamp: float timestamp_iso: datetime = FieldInfo(alias="timestampIso") diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py index a923d98..1f3c1bc 100644 --- a/src/ark/types/email_retrieve_deliveries_response.py +++ b/src/ark/types/email_retrieve_deliveries_response.py @@ -170,6 +170,9 @@ class Data(BaseModel): - `bounced` - Bounced by recipient server """ + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email belongs to""" + class EmailRetrieveDeliveriesResponse(BaseModel): data: Data diff --git a/src/ark/types/email_retrieve_response.py b/src/ark/types/email_retrieve_response.py index d9bc54c..c0de6b1 100644 --- a/src/ark/types/email_retrieve_response.py +++ b/src/ark/types/email_retrieve_response.py @@ -179,6 +179,9 @@ class Data(BaseModel): subject: str """Email subject line""" + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email belongs to""" + timestamp: float """Unix timestamp when the email was sent""" diff --git a/src/ark/types/email_retry_response.py b/src/ark/types/email_retry_response.py index f63cc99..fe5d8ce 100644 --- a/src/ark/types/email_retry_response.py +++ b/src/ark/types/email_retry_response.py @@ -2,6 +2,8 @@ from typing_extensions import Literal +from pydantic import Field as FieldInfo + from .._models import BaseModel from .shared.api_meta import APIMeta @@ -14,6 +16,9 @@ class Data(BaseModel): message: str + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email belongs to""" + class EmailRetryResponse(BaseModel): data: Data diff --git a/src/ark/types/email_send_batch_params.py b/src/ark/types/email_send_batch_params.py index ba54d41..38c9caf 100644 --- a/src/ark/types/email_send_batch_params.py +++ b/src/ark/types/email_send_batch_params.py @@ -17,6 +17,17 @@ class EmailSendBatchParams(TypedDict, total=False): from_: Required[Annotated[str, PropertyInfo(alias="from")]] """Sender email for all messages""" + tenant_id: Annotated[Optional[str], PropertyInfo(alias="tenantId")] + """The tenant ID to send this batch from. + + Determines which tenant's configuration (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + """ + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/ark/types/email_send_batch_response.py b/src/ark/types/email_send_batch_response.py index d32e3fc..06fe526 100644 --- a/src/ark/types/email_send_batch_response.py +++ b/src/ark/types/email_send_batch_response.py @@ -3,6 +3,8 @@ from typing import Dict, Optional from typing_extensions import Literal +from pydantic import Field as FieldInfo + from .._models import BaseModel from .shared.api_meta import APIMeta @@ -24,6 +26,9 @@ class Data(BaseModel): messages: Dict[str, DataMessages] """Map of recipient email to message info""" + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this batch was sent from""" + total: int """Total emails in the batch""" diff --git a/src/ark/types/email_send_params.py b/src/ark/types/email_send_params.py index 0363339..af3e94f 100644 --- a/src/ark/types/email_send_params.py +++ b/src/ark/types/email_send_params.py @@ -79,6 +79,17 @@ class EmailSendParams(TypedDict, total=False): tag: Optional[str] """Tag for categorization and filtering (accepts null)""" + tenant_id: Annotated[Optional[str], PropertyInfo(alias="tenantId")] + """The tenant ID to send this email from. + + Determines which tenant's configuration (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + """ + text: Optional[str] """ Plain text body (accepts null, auto-generated from HTML if not provided). diff --git a/src/ark/types/email_send_raw_params.py b/src/ark/types/email_send_raw_params.py index d957f74..56c038b 100644 --- a/src/ark/types/email_send_raw_params.py +++ b/src/ark/types/email_send_raw_params.py @@ -37,3 +37,14 @@ class EmailSendRawParams(TypedDict, total=False): bounce: Optional[bool] """Whether this is a bounce message (accepts null)""" + + tenant_id: Annotated[Optional[str], PropertyInfo(alias="tenantId")] + """The tenant ID to send this email from. + + Determines which tenant's configuration (domains, webhooks, tracking) is used. + + - If your API key is scoped to a specific tenant, this must match that tenant or + be omitted. + - If your API key is org-level, specify the tenant to send from. + - If omitted, the organization's default tenant is used. + """ diff --git a/src/ark/types/email_send_raw_response.py b/src/ark/types/email_send_raw_response.py index 46e7c96..80a61fb 100644 --- a/src/ark/types/email_send_raw_response.py +++ b/src/ark/types/email_send_raw_response.py @@ -18,6 +18,9 @@ class Data(BaseModel): status: Literal["pending", "sent"] """Current delivery status""" + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email was sent from""" + to: List[str] """List of recipient addresses""" diff --git a/src/ark/types/email_send_response.py b/src/ark/types/email_send_response.py index cb7d814..7c54d65 100644 --- a/src/ark/types/email_send_response.py +++ b/src/ark/types/email_send_response.py @@ -18,6 +18,9 @@ class Data(BaseModel): status: Literal["pending", "sent"] """Current delivery status""" + tenant_id: str = FieldInfo(alias="tenantId") + """The tenant ID this email was sent from""" + to: List[str] """List of recipient addresses""" diff --git a/tests/api_resources/test_emails.py b/tests/api_resources/test_emails.py index 0e37b72..daa4744 100644 --- a/tests/api_resources/test_emails.py +++ b/tests/api_resources/test_emails.py @@ -219,6 +219,7 @@ def test_method_send_with_all_params(self, client: Ark) -> None: }, reply_to="dev@stainless.com", tag="tag", + tenant_id="cm6abc123def456", text="text", idempotency_key="user_123_order_456", ) @@ -299,6 +300,7 @@ def test_method_send_batch_with_all_params(self, client: Ark) -> None: }, ], from_="notifications@myapp.com", + tenant_id="cm6abc123def456", idempotency_key="user_123_order_456", ) assert_matches_type(EmailSendBatchResponse, email, path=["response"]) @@ -363,6 +365,7 @@ def test_method_send_raw_with_all_params(self, client: Ark) -> None: raw_message="x", to=["user@example.com"], bounce=True, + tenant_id="cm6abc123def456", ) assert_matches_type(EmailSendRawResponse, email, path=["response"]) @@ -593,6 +596,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncArk) -> None }, reply_to="dev@stainless.com", tag="tag", + tenant_id="cm6abc123def456", text="text", idempotency_key="user_123_order_456", ) @@ -673,6 +677,7 @@ async def test_method_send_batch_with_all_params(self, async_client: AsyncArk) - }, ], from_="notifications@myapp.com", + tenant_id="cm6abc123def456", idempotency_key="user_123_order_456", ) assert_matches_type(EmailSendBatchResponse, email, path=["response"]) @@ -737,6 +742,7 @@ async def test_method_send_raw_with_all_params(self, async_client: AsyncArk) -> raw_message="x", to=["user@example.com"], bounce=True, + tenant_id="cm6abc123def456", ) assert_matches_type(EmailSendRawResponse, email, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index 73b2133..ead5154 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -951,6 +951,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1859,6 +1867,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient()