From 0ff55caabeb38ad0046cf489c28f8dad3caddfc2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:32:42 +0000
Subject: [PATCH 1/6] feat(api): add Credentials endpoint
---
.stats.yml | 8 +-
api.md | 34 +-
src/ark/_client.py | 2 +-
src/ark/resources/domains.py | 82 ++-
src/ark/resources/tenants/__init__.py | 33 +
src/ark/resources/tenants/credentials.py | 679 ++++++++++++++++++
src/ark/resources/{ => tenants}/tenants.py | 58 +-
src/ark/types/__init__.py | 1 +
src/ark/types/domain_create_params.py | 3 +
src/ark/types/domain_create_response.py | 6 +
src/ark/types/domain_list_params.py | 12 +
src/ark/types/domain_list_response.py | 10 +-
src/ark/types/domain_retrieve_response.py | 6 +
src/ark/types/domain_verify_response.py | 6 +
src/ark/types/tenants/__init__.py | 13 +
.../types/tenants/credential_create_params.py | 22 +
.../tenants/credential_create_response.py | 63 ++
.../tenants/credential_delete_response.py | 20 +
.../types/tenants/credential_list_params.py | 20 +
.../types/tenants/credential_list_response.py | 41 ++
.../tenants/credential_retrieve_params.py | 16 +
.../tenants/credential_retrieve_response.py | 63 ++
.../types/tenants/credential_update_params.py | 22 +
.../tenants/credential_update_response.py | 63 ++
tests/api_resources/tenants/__init__.py | 1 +
.../api_resources/tenants/test_credentials.py | 509 +++++++++++++
tests/api_resources/test_domains.py | 20 +
27 files changed, 1780 insertions(+), 33 deletions(-)
create mode 100644 src/ark/resources/tenants/__init__.py
create mode 100644 src/ark/resources/tenants/credentials.py
rename src/ark/resources/{ => tenants}/tenants.py (91%)
create mode 100644 src/ark/types/domain_list_params.py
create mode 100644 src/ark/types/tenants/__init__.py
create mode 100644 src/ark/types/tenants/credential_create_params.py
create mode 100644 src/ark/types/tenants/credential_create_response.py
create mode 100644 src/ark/types/tenants/credential_delete_response.py
create mode 100644 src/ark/types/tenants/credential_list_params.py
create mode 100644 src/ark/types/tenants/credential_list_response.py
create mode 100644 src/ark/types/tenants/credential_retrieve_params.py
create mode 100644 src/ark/types/tenants/credential_retrieve_response.py
create mode 100644 src/ark/types/tenants/credential_update_params.py
create mode 100644 src/ark/types/tenants/credential_update_response.py
create mode 100644 tests/api_resources/tenants/__init__.py
create mode 100644 tests/api_resources/tenants/test_credentials.py
diff --git a/.stats.yml b/.stats.yml
index 576e5df..27d9c12 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 40
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-628db0b9b7c9da594fa6ad6ce9d95f4ecad92c9e0313f2f1f9977216494dbc5d.yml
-openapi_spec_hash: 1773341fbff31b84d2cbcdb37eaad877
-config_hash: b090c2bdd7a719c56c825edddc587737
+configured_endpoints: 45
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-3aa940f5978d27c19f21fd584c87674f7a1e2bf4495fbd9253e60e7b8e9d28ea.yml
+openapi_spec_hash: 7354939360962eb34b88189afbe97222
+config_hash: 7b825ebe29bcb14e63e718c041b8398c
diff --git a/api.md b/api.md
index 19e5c9f..519ab85 100644
--- a/api.md
+++ b/api.md
@@ -49,7 +49,7 @@ Methods:
- client.domains.create(\*\*params) -> DomainCreateResponse
- client.domains.retrieve(domain_id) -> DomainRetrieveResponse
-- client.domains.list() -> DomainListResponse
+- client.domains.list(\*\*params) -> DomainListResponse
- client.domains.delete(domain_id) -> DomainDeleteResponse
- client.domains.verify(domain_id) -> DomainVerifyResponse
@@ -171,8 +171,30 @@ from ark.types import (
Methods:
-- client.tenants.create(\*\*params) -> TenantCreateResponse
-- client.tenants.retrieve(tenant_id) -> TenantRetrieveResponse
-- client.tenants.update(tenant_id, \*\*params) -> TenantUpdateResponse
-- client.tenants.list(\*\*params) -> SyncPageNumberPagination[Tenant]
-- client.tenants.delete(tenant_id) -> TenantDeleteResponse
+- client.tenants.create(\*\*params) -> TenantCreateResponse
+- client.tenants.retrieve(tenant_id) -> TenantRetrieveResponse
+- client.tenants.update(tenant_id, \*\*params) -> TenantUpdateResponse
+- client.tenants.list(\*\*params) -> SyncPageNumberPagination[Tenant]
+- client.tenants.delete(tenant_id) -> TenantDeleteResponse
+
+## Credentials
+
+Types:
+
+```python
+from ark.types.tenants import (
+ CredentialCreateResponse,
+ CredentialRetrieveResponse,
+ CredentialUpdateResponse,
+ CredentialListResponse,
+ CredentialDeleteResponse,
+)
+```
+
+Methods:
+
+- client.tenants.credentials.create(tenant_id, \*\*params) -> CredentialCreateResponse
+- client.tenants.credentials.retrieve(credential_id, \*, tenant_id, \*\*params) -> CredentialRetrieveResponse
+- client.tenants.credentials.update(credential_id, \*, tenant_id, \*\*params) -> CredentialUpdateResponse
+- client.tenants.credentials.list(tenant_id, \*\*params) -> SyncPageNumberPagination[CredentialListResponse]
+- client.tenants.credentials.delete(credential_id, \*, tenant_id) -> CredentialDeleteResponse
diff --git a/src/ark/_client.py b/src/ark/_client.py
index 51fb70d..4958103 100644
--- a/src/ark/_client.py
+++ b/src/ark/_client.py
@@ -36,10 +36,10 @@
from .resources.usage import UsageResource, AsyncUsageResource
from .resources.emails import EmailsResource, AsyncEmailsResource
from .resources.domains import DomainsResource, AsyncDomainsResource
- from .resources.tenants import TenantsResource, AsyncTenantsResource
from .resources.tracking import TrackingResource, AsyncTrackingResource
from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
from .resources.suppressions import SuppressionsResource, AsyncSuppressionsResource
+ from .resources.tenants.tenants import TenantsResource, AsyncTenantsResource
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Ark", "AsyncArk", "Client", "AsyncClient"]
diff --git a/src/ark/resources/domains.py b/src/ark/resources/domains.py
index ff0fadc..89c1076 100644
--- a/src/ark/resources/domains.py
+++ b/src/ark/resources/domains.py
@@ -4,8 +4,8 @@
import httpx
-from ..types import domain_create_params
-from .._types import Body, Query, Headers, NotGiven, not_given
+from ..types import domain_list_params, domain_create_params
+from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -49,6 +49,7 @@ def create(
self,
*,
name: str,
+ tenant_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,
@@ -61,6 +62,9 @@ def create(
Returns DNS records that must be configured
before the domain can be verified.
+ **Required:** `tenant_id` to specify which tenant the domain belongs to. Each
+ tenant gets their own isolated mail server for domain isolation.
+
**Required DNS records:**
- **SPF** - TXT record for sender authentication
@@ -72,6 +76,8 @@ def create(
Args:
name: Domain name (e.g., "mail.example.com")
+ tenant_id: ID of the tenant this domain belongs to
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -82,7 +88,13 @@ def create(
"""
return self._post(
"/domains",
- body=maybe_transform({"name": name}, domain_create_params.DomainCreateParams),
+ body=maybe_transform(
+ {
+ "name": name,
+ "tenant_id": tenant_id,
+ },
+ domain_create_params.DomainCreateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -125,6 +137,7 @@ def retrieve(
def list(
self,
*,
+ tenant_id: 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,
@@ -132,11 +145,31 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainListResponse:
- """Get all sending domains with their verification status"""
+ """
+ Get all sending domains with their verification status.
+
+ Optionally filter by `tenant_id` to list domains for a specific tenant. When
+ filtered, response includes `tenant_id` and `tenant_name` for each domain.
+
+ Args:
+ tenant_id: Filter domains by tenant ID
+
+ 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
+ """
return self._get(
"/domains",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"tenant_id": tenant_id}, domain_list_params.DomainListParams),
),
cast_to=DomainListResponse,
)
@@ -240,6 +273,7 @@ async def create(
self,
*,
name: str,
+ tenant_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,
@@ -252,6 +286,9 @@ async def create(
Returns DNS records that must be configured
before the domain can be verified.
+ **Required:** `tenant_id` to specify which tenant the domain belongs to. Each
+ tenant gets their own isolated mail server for domain isolation.
+
**Required DNS records:**
- **SPF** - TXT record for sender authentication
@@ -263,6 +300,8 @@ async def create(
Args:
name: Domain name (e.g., "mail.example.com")
+ tenant_id: ID of the tenant this domain belongs to
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -273,7 +312,13 @@ async def create(
"""
return await self._post(
"/domains",
- body=await async_maybe_transform({"name": name}, domain_create_params.DomainCreateParams),
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "tenant_id": tenant_id,
+ },
+ domain_create_params.DomainCreateParams,
+ ),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -316,6 +361,7 @@ async def retrieve(
async def list(
self,
*,
+ tenant_id: 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,
@@ -323,11 +369,31 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainListResponse:
- """Get all sending domains with their verification status"""
+ """
+ Get all sending domains with their verification status.
+
+ Optionally filter by `tenant_id` to list domains for a specific tenant. When
+ filtered, response includes `tenant_id` and `tenant_name` for each domain.
+
+ Args:
+ tenant_id: Filter domains by tenant ID
+
+ 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
+ """
return await self._get(
"/domains",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"tenant_id": tenant_id}, domain_list_params.DomainListParams),
),
cast_to=DomainListResponse,
)
diff --git a/src/ark/resources/tenants/__init__.py b/src/ark/resources/tenants/__init__.py
new file mode 100644
index 0000000..f05ec88
--- /dev/null
+++ b/src/ark/resources/tenants/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .tenants import (
+ TenantsResource,
+ AsyncTenantsResource,
+ TenantsResourceWithRawResponse,
+ AsyncTenantsResourceWithRawResponse,
+ TenantsResourceWithStreamingResponse,
+ AsyncTenantsResourceWithStreamingResponse,
+)
+from .credentials import (
+ CredentialsResource,
+ AsyncCredentialsResource,
+ CredentialsResourceWithRawResponse,
+ AsyncCredentialsResourceWithRawResponse,
+ CredentialsResourceWithStreamingResponse,
+ AsyncCredentialsResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "CredentialsResource",
+ "AsyncCredentialsResource",
+ "CredentialsResourceWithRawResponse",
+ "AsyncCredentialsResourceWithRawResponse",
+ "CredentialsResourceWithStreamingResponse",
+ "AsyncCredentialsResourceWithStreamingResponse",
+ "TenantsResource",
+ "AsyncTenantsResource",
+ "TenantsResourceWithRawResponse",
+ "AsyncTenantsResourceWithRawResponse",
+ "TenantsResourceWithStreamingResponse",
+ "AsyncTenantsResourceWithStreamingResponse",
+]
diff --git a/src/ark/resources/tenants/credentials.py b/src/ark/resources/tenants/credentials.py
new file mode 100644
index 0000000..98aed9b
--- /dev/null
+++ b/src/ark/resources/tenants/credentials.py
@@ -0,0 +1,679 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.tenants import (
+ credential_list_params,
+ credential_create_params,
+ credential_update_params,
+ credential_retrieve_params,
+)
+from ...types.tenants.credential_list_response import CredentialListResponse
+from ...types.tenants.credential_create_response import CredentialCreateResponse
+from ...types.tenants.credential_delete_response import CredentialDeleteResponse
+from ...types.tenants.credential_update_response import CredentialUpdateResponse
+from ...types.tenants.credential_retrieve_response import CredentialRetrieveResponse
+
+__all__ = ["CredentialsResource", "AsyncCredentialsResource"]
+
+
+class CredentialsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> CredentialsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return CredentialsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> CredentialsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return CredentialsResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ tenant_id: str,
+ *,
+ name: str,
+ type: Literal["smtp", "api"],
+ # 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,
+ ) -> CredentialCreateResponse:
+ """Create a new SMTP or API credential for a tenant.
+
+ The credential can be used to
+ send emails through Postal on behalf of the tenant.
+
+ **Important:** The credential key is only returned once at creation time. Store
+ it securely - you cannot retrieve it again.
+
+ **Credential Types:**
+
+ - `smtp` - For SMTP-based email sending. Returns both `key` and `smtpUsername`.
+ - `api` - For API-based email sending. Returns only `key`.
+
+ Args:
+ name: Name for the credential. Can only contain letters, numbers, hyphens, and
+ underscores. Max 50 characters.
+
+ type:
+ Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._post(
+ f"/tenants/{tenant_id}/credentials",
+ body=maybe_transform(
+ {
+ "name": name,
+ "type": type,
+ },
+ credential_create_params.CredentialCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialCreateResponse,
+ )
+
+ def retrieve(
+ self,
+ credential_id: int,
+ *,
+ tenant_id: str,
+ reveal: bool | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CredentialRetrieveResponse:
+ """
+ Get details of a specific credential.
+
+ **Revealing the key:** By default, the credential key is not returned. Pass
+ `reveal=true` to include the key in the response. Use this sparingly and only
+ when you need to retrieve the key (e.g., for configuration).
+
+ Args:
+ reveal: Set to `true` to include the credential key in the response
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"reveal": reveal}, credential_retrieve_params.CredentialRetrieveParams),
+ ),
+ cast_to=CredentialRetrieveResponse,
+ )
+
+ def update(
+ self,
+ credential_id: int,
+ *,
+ tenant_id: str,
+ hold: bool | Omit = omit,
+ name: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CredentialUpdateResponse:
+ """
+ Update a credential's name or hold status.
+
+ **Hold Status:**
+
+ - When `hold: true`, the credential is disabled and cannot be used to send
+ emails.
+ - When `hold: false`, the credential is active and can send emails.
+ - Use this to temporarily disable a credential without deleting it.
+
+ Args:
+ hold: Set to `true` to disable the credential (put on hold). Set to `false` to enable
+ the credential (release from hold).
+
+ name: New name for the credential
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._patch(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ body=maybe_transform(
+ {
+ "hold": hold,
+ "name": name,
+ },
+ credential_update_params.CredentialUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialUpdateResponse,
+ )
+
+ def list(
+ self,
+ tenant_id: str,
+ *,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ type: Literal["smtp", "api"] | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncPageNumberPagination[CredentialListResponse]:
+ """List all SMTP and API credentials for a tenant.
+
+ Credentials are used to send
+ emails through Postal on behalf of the tenant.
+
+ **Security:** Credential keys are not returned in the list response. Use the
+ retrieve endpoint with `reveal=true` to get the key.
+
+ Args:
+ page: Page number (1-indexed)
+
+ per_page: Number of items per page (max 100)
+
+ type: Filter by credential type
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get_api_list(
+ f"/tenants/{tenant_id}/credentials",
+ page=SyncPageNumberPagination[CredentialListResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "page": page,
+ "per_page": per_page,
+ "type": type,
+ },
+ credential_list_params.CredentialListParams,
+ ),
+ ),
+ model=CredentialListResponse,
+ )
+
+ def delete(
+ self,
+ credential_id: int,
+ *,
+ tenant_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,
+ ) -> CredentialDeleteResponse:
+ """Permanently delete (revoke) a credential.
+
+ The credential can no longer be used
+ to send emails.
+
+ **Warning:** This action is irreversible. If you want to temporarily disable a
+ credential, use the update endpoint to set `hold: true` instead.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._delete(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialDeleteResponse,
+ )
+
+
+class AsyncCredentialsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncCredentialsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncCredentialsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncCredentialsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncCredentialsResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ tenant_id: str,
+ *,
+ name: str,
+ type: Literal["smtp", "api"],
+ # 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,
+ ) -> CredentialCreateResponse:
+ """Create a new SMTP or API credential for a tenant.
+
+ The credential can be used to
+ send emails through Postal on behalf of the tenant.
+
+ **Important:** The credential key is only returned once at creation time. Store
+ it securely - you cannot retrieve it again.
+
+ **Credential Types:**
+
+ - `smtp` - For SMTP-based email sending. Returns both `key` and `smtpUsername`.
+ - `api` - For API-based email sending. Returns only `key`.
+
+ Args:
+ name: Name for the credential. Can only contain letters, numbers, hyphens, and
+ underscores. Max 50 characters.
+
+ type:
+ Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._post(
+ f"/tenants/{tenant_id}/credentials",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "type": type,
+ },
+ credential_create_params.CredentialCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialCreateResponse,
+ )
+
+ async def retrieve(
+ self,
+ credential_id: int,
+ *,
+ tenant_id: str,
+ reveal: bool | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CredentialRetrieveResponse:
+ """
+ Get details of a specific credential.
+
+ **Revealing the key:** By default, the credential key is not returned. Pass
+ `reveal=true` to include the key in the response. Use this sparingly and only
+ when you need to retrieve the key (e.g., for configuration).
+
+ Args:
+ reveal: Set to `true` to include the credential key in the response
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {"reveal": reveal}, credential_retrieve_params.CredentialRetrieveParams
+ ),
+ ),
+ cast_to=CredentialRetrieveResponse,
+ )
+
+ async def update(
+ self,
+ credential_id: int,
+ *,
+ tenant_id: str,
+ hold: bool | Omit = omit,
+ name: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> CredentialUpdateResponse:
+ """
+ Update a credential's name or hold status.
+
+ **Hold Status:**
+
+ - When `hold: true`, the credential is disabled and cannot be used to send
+ emails.
+ - When `hold: false`, the credential is active and can send emails.
+ - Use this to temporarily disable a credential without deleting it.
+
+ Args:
+ hold: Set to `true` to disable the credential (put on hold). Set to `false` to enable
+ the credential (release from hold).
+
+ name: New name for the credential
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._patch(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ body=await async_maybe_transform(
+ {
+ "hold": hold,
+ "name": name,
+ },
+ credential_update_params.CredentialUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialUpdateResponse,
+ )
+
+ def list(
+ self,
+ tenant_id: str,
+ *,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ type: Literal["smtp", "api"] | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[CredentialListResponse, AsyncPageNumberPagination[CredentialListResponse]]:
+ """List all SMTP and API credentials for a tenant.
+
+ Credentials are used to send
+ emails through Postal on behalf of the tenant.
+
+ **Security:** Credential keys are not returned in the list response. Use the
+ retrieve endpoint with `reveal=true` to get the key.
+
+ Args:
+ page: Page number (1-indexed)
+
+ per_page: Number of items per page (max 100)
+
+ type: Filter by credential type
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get_api_list(
+ f"/tenants/{tenant_id}/credentials",
+ page=AsyncPageNumberPagination[CredentialListResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "page": page,
+ "per_page": per_page,
+ "type": type,
+ },
+ credential_list_params.CredentialListParams,
+ ),
+ ),
+ model=CredentialListResponse,
+ )
+
+ async def delete(
+ self,
+ credential_id: int,
+ *,
+ tenant_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,
+ ) -> CredentialDeleteResponse:
+ """Permanently delete (revoke) a credential.
+
+ The credential can no longer be used
+ to send emails.
+
+ **Warning:** This action is irreversible. If you want to temporarily disable a
+ credential, use the update endpoint to set `hold: true` instead.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._delete(
+ f"/tenants/{tenant_id}/credentials/{credential_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=CredentialDeleteResponse,
+ )
+
+
+class CredentialsResourceWithRawResponse:
+ def __init__(self, credentials: CredentialsResource) -> None:
+ self._credentials = credentials
+
+ self.create = to_raw_response_wrapper(
+ credentials.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ credentials.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ credentials.update,
+ )
+ self.list = to_raw_response_wrapper(
+ credentials.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ credentials.delete,
+ )
+
+
+class AsyncCredentialsResourceWithRawResponse:
+ def __init__(self, credentials: AsyncCredentialsResource) -> None:
+ self._credentials = credentials
+
+ self.create = async_to_raw_response_wrapper(
+ credentials.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ credentials.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ credentials.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ credentials.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ credentials.delete,
+ )
+
+
+class CredentialsResourceWithStreamingResponse:
+ def __init__(self, credentials: CredentialsResource) -> None:
+ self._credentials = credentials
+
+ self.create = to_streamed_response_wrapper(
+ credentials.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ credentials.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ credentials.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ credentials.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ credentials.delete,
+ )
+
+
+class AsyncCredentialsResourceWithStreamingResponse:
+ def __init__(self, credentials: AsyncCredentialsResource) -> None:
+ self._credentials = credentials
+
+ self.create = async_to_streamed_response_wrapper(
+ credentials.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ credentials.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ credentials.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ credentials.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ credentials.delete,
+ )
diff --git a/src/ark/resources/tenants.py b/src/ark/resources/tenants/tenants.py
similarity index 91%
rename from src/ark/resources/tenants.py
rename to src/ark/resources/tenants/tenants.py
index e7b13a1..9c073a5 100644
--- a/src/ark/resources/tenants.py
+++ b/src/ark/resources/tenants/tenants.py
@@ -7,29 +7,41 @@
import httpx
-from ..types import tenant_list_params, tenant_create_params, tenant_update_params
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ...types import tenant_list_params, tenant_create_params, tenant_update_params
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncPageNumberPagination, AsyncPageNumberPagination
-from .._base_client import AsyncPaginator, make_request_options
-from ..types.tenant import Tenant
-from ..types.tenant_create_response import TenantCreateResponse
-from ..types.tenant_delete_response import TenantDeleteResponse
-from ..types.tenant_update_response import TenantUpdateResponse
-from ..types.tenant_retrieve_response import TenantRetrieveResponse
+from .credentials import (
+ CredentialsResource,
+ AsyncCredentialsResource,
+ CredentialsResourceWithRawResponse,
+ AsyncCredentialsResourceWithRawResponse,
+ CredentialsResourceWithStreamingResponse,
+ AsyncCredentialsResourceWithStreamingResponse,
+)
+from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.tenant import Tenant
+from ...types.tenant_create_response import TenantCreateResponse
+from ...types.tenant_delete_response import TenantDeleteResponse
+from ...types.tenant_update_response import TenantUpdateResponse
+from ...types.tenant_retrieve_response import TenantRetrieveResponse
__all__ = ["TenantsResource", "AsyncTenantsResource"]
class TenantsResource(SyncAPIResource):
+ @cached_property
+ def credentials(self) -> CredentialsResource:
+ return CredentialsResource(self._client)
+
@cached_property
def with_raw_response(self) -> TenantsResourceWithRawResponse:
"""
@@ -284,6 +296,10 @@ def delete(
class AsyncTenantsResource(AsyncAPIResource):
+ @cached_property
+ def credentials(self) -> AsyncCredentialsResource:
+ return AsyncCredentialsResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncTenantsResourceWithRawResponse:
"""
@@ -557,6 +573,10 @@ def __init__(self, tenants: TenantsResource) -> None:
tenants.delete,
)
+ @cached_property
+ def credentials(self) -> CredentialsResourceWithRawResponse:
+ return CredentialsResourceWithRawResponse(self._tenants.credentials)
+
class AsyncTenantsResourceWithRawResponse:
def __init__(self, tenants: AsyncTenantsResource) -> None:
@@ -578,6 +598,10 @@ def __init__(self, tenants: AsyncTenantsResource) -> None:
tenants.delete,
)
+ @cached_property
+ def credentials(self) -> AsyncCredentialsResourceWithRawResponse:
+ return AsyncCredentialsResourceWithRawResponse(self._tenants.credentials)
+
class TenantsResourceWithStreamingResponse:
def __init__(self, tenants: TenantsResource) -> None:
@@ -599,6 +623,10 @@ def __init__(self, tenants: TenantsResource) -> None:
tenants.delete,
)
+ @cached_property
+ def credentials(self) -> CredentialsResourceWithStreamingResponse:
+ return CredentialsResourceWithStreamingResponse(self._tenants.credentials)
+
class AsyncTenantsResourceWithStreamingResponse:
def __init__(self, tenants: AsyncTenantsResource) -> None:
@@ -619,3 +647,7 @@ def __init__(self, tenants: AsyncTenantsResource) -> None:
self.delete = async_to_streamed_response_wrapper(
tenants.delete,
)
+
+ @cached_property
+ def credentials(self) -> AsyncCredentialsResourceWithStreamingResponse:
+ return AsyncCredentialsResourceWithStreamingResponse(self._tenants.credentials)
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index 23e9cb2..42467e4 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -11,6 +11,7 @@
from .log_entry_detail import LogEntryDetail as LogEntryDetail
from .email_list_params import EmailListParams as EmailListParams
from .email_send_params import EmailSendParams as EmailSendParams
+from .domain_list_params import DomainListParams as DomainListParams
from .tenant_list_params import TenantListParams as TenantListParams
from .email_list_response import EmailListResponse as EmailListResponse
from .email_send_response import EmailSendResponse as EmailSendResponse
diff --git a/src/ark/types/domain_create_params.py b/src/ark/types/domain_create_params.py
index 13a4fcb..338fb37 100644
--- a/src/ark/types/domain_create_params.py
+++ b/src/ark/types/domain_create_params.py
@@ -10,3 +10,6 @@
class DomainCreateParams(TypedDict, total=False):
name: Required[str]
"""Domain name (e.g., "mail.example.com")"""
+
+ tenant_id: Required[str]
+ """ID of the tenant this domain belongs to"""
diff --git a/src/ark/types/domain_create_response.py b/src/ark/types/domain_create_response.py
index 56fdef4..f00f9fa 100644
--- a/src/ark/types/domain_create_response.py
+++ b/src/ark/types/domain_create_response.py
@@ -124,6 +124,12 @@ class Data(BaseModel):
Domain must be verified before sending emails.
"""
+ tenant_id: Optional[str] = None
+ """ID of the tenant this domain belongs to"""
+
+ tenant_name: Optional[str] = None
+ """Name of the tenant this domain belongs to"""
+
verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None)
"""Timestamp when the domain ownership was verified, or null if not yet verified"""
diff --git a/src/ark/types/domain_list_params.py b/src/ark/types/domain_list_params.py
new file mode 100644
index 0000000..b0447f0
--- /dev/null
+++ b/src/ark/types/domain_list_params.py
@@ -0,0 +1,12 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["DomainListParams"]
+
+
+class DomainListParams(TypedDict, total=False):
+ tenant_id: str
+ """Filter domains by tenant ID"""
diff --git a/src/ark/types/domain_list_response.py b/src/ark/types/domain_list_response.py
index 50edc86..1ae8a49 100644
--- a/src/ark/types/domain_list_response.py
+++ b/src/ark/types/domain_list_response.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List
+from typing import List, Optional
from typing_extensions import Literal
from .._models import BaseModel
@@ -22,6 +22,14 @@ class DataDomain(BaseModel):
Domain must be verified before sending emails.
"""
+ tenant_id: Optional[str] = None
+ """ID of the tenant this domain belongs to (included when filtering by tenant_id)"""
+
+ tenant_name: Optional[str] = None
+ """
+ Name of the tenant this domain belongs to (included when filtering by tenant_id)
+ """
+
class Data(BaseModel):
domains: List[DataDomain]
diff --git a/src/ark/types/domain_retrieve_response.py b/src/ark/types/domain_retrieve_response.py
index bc9d550..7eecf9d 100644
--- a/src/ark/types/domain_retrieve_response.py
+++ b/src/ark/types/domain_retrieve_response.py
@@ -124,6 +124,12 @@ class Data(BaseModel):
Domain must be verified before sending emails.
"""
+ tenant_id: Optional[str] = None
+ """ID of the tenant this domain belongs to"""
+
+ tenant_name: Optional[str] = None
+ """Name of the tenant this domain belongs to"""
+
verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None)
"""Timestamp when the domain ownership was verified, or null if not yet verified"""
diff --git a/src/ark/types/domain_verify_response.py b/src/ark/types/domain_verify_response.py
index 8bc1ae9..a85bcf9 100644
--- a/src/ark/types/domain_verify_response.py
+++ b/src/ark/types/domain_verify_response.py
@@ -124,6 +124,12 @@ class Data(BaseModel):
Domain must be verified before sending emails.
"""
+ tenant_id: Optional[str] = None
+ """ID of the tenant this domain belongs to"""
+
+ tenant_name: Optional[str] = None
+ """Name of the tenant this domain belongs to"""
+
verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None)
"""Timestamp when the domain ownership was verified, or null if not yet verified"""
diff --git a/src/ark/types/tenants/__init__.py b/src/ark/types/tenants/__init__.py
new file mode 100644
index 0000000..01a33b1
--- /dev/null
+++ b/src/ark/types/tenants/__init__.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .credential_list_params import CredentialListParams as CredentialListParams
+from .credential_create_params import CredentialCreateParams as CredentialCreateParams
+from .credential_list_response import CredentialListResponse as CredentialListResponse
+from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams
+from .credential_create_response import CredentialCreateResponse as CredentialCreateResponse
+from .credential_delete_response import CredentialDeleteResponse as CredentialDeleteResponse
+from .credential_retrieve_params import CredentialRetrieveParams as CredentialRetrieveParams
+from .credential_update_response import CredentialUpdateResponse as CredentialUpdateResponse
+from .credential_retrieve_response import CredentialRetrieveResponse as CredentialRetrieveResponse
diff --git a/src/ark/types/tenants/credential_create_params.py b/src/ark/types/tenants/credential_create_params.py
new file mode 100644
index 0000000..3bbcc2d
--- /dev/null
+++ b/src/ark/types/tenants/credential_create_params.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["CredentialCreateParams"]
+
+
+class CredentialCreateParams(TypedDict, total=False):
+ name: Required[str]
+ """Name for the credential.
+
+ Can only contain letters, numbers, hyphens, and underscores. Max 50 characters.
+ """
+
+ type: Required[Literal["smtp", "api"]]
+ """Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+ """
diff --git a/src/ark/types/tenants/credential_create_response.py b/src/ark/types/tenants/credential_create_response.py
new file mode 100644
index 0000000..685e534
--- /dev/null
+++ b/src/ark/types/tenants/credential_create_response.py
@@ -0,0 +1,63 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["CredentialCreateResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: int
+ """Unique identifier for the credential"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+ """When the credential was created"""
+
+ hold: bool
+ """
+ Whether the credential is on hold (disabled). When `true`, the credential cannot
+ be used to send emails.
+ """
+
+ key: str
+ """The credential key (secret).
+
+ **Store this securely** - it will not be shown again unless you use the reveal
+ parameter.
+ """
+
+ last_used_at: Optional[datetime] = FieldInfo(alias="lastUsedAt", default=None)
+ """When the credential was last used to send an email"""
+
+ name: str
+ """Name of the credential"""
+
+ type: Literal["smtp", "api"]
+ """Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+ """
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+ """When the credential was last updated"""
+
+ smtp_username: Optional[str] = FieldInfo(alias="smtpUsername", default=None)
+ """SMTP username for authentication.
+
+ Only included for SMTP credentials. Format: `{tenantId}/{key}`
+ """
+
+
+class CredentialCreateResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenants/credential_delete_response.py b/src/ark/types/tenants/credential_delete_response.py
new file mode 100644
index 0000000..772b55e
--- /dev/null
+++ b/src/ark/types/tenants/credential_delete_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["CredentialDeleteResponse", "Data"]
+
+
+class Data(BaseModel):
+ deleted: Literal[True]
+
+
+class CredentialDeleteResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenants/credential_list_params.py b/src/ark/types/tenants/credential_list_params.py
new file mode 100644
index 0000000..29245af
--- /dev/null
+++ b/src/ark/types/tenants/credential_list_params.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["CredentialListParams"]
+
+
+class CredentialListParams(TypedDict, total=False):
+ page: int
+ """Page number (1-indexed)"""
+
+ per_page: Annotated[int, PropertyInfo(alias="perPage")]
+ """Number of items per page (max 100)"""
+
+ type: Literal["smtp", "api"]
+ """Filter by credential type"""
diff --git a/src/ark/types/tenants/credential_list_response.py b/src/ark/types/tenants/credential_list_response.py
new file mode 100644
index 0000000..f69e705
--- /dev/null
+++ b/src/ark/types/tenants/credential_list_response.py
@@ -0,0 +1,41 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = ["CredentialListResponse"]
+
+
+class CredentialListResponse(BaseModel):
+ id: int
+ """Unique identifier for the credential"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+ """When the credential was created"""
+
+ hold: bool
+ """
+ Whether the credential is on hold (disabled). When `true`, the credential cannot
+ be used to send emails.
+ """
+
+ last_used_at: Optional[datetime] = FieldInfo(alias="lastUsedAt", default=None)
+ """When the credential was last used to send an email"""
+
+ name: str
+ """Name of the credential"""
+
+ type: Literal["smtp", "api"]
+ """Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+ """
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+ """When the credential was last updated"""
diff --git a/src/ark/types/tenants/credential_retrieve_params.py b/src/ark/types/tenants/credential_retrieve_params.py
new file mode 100644
index 0000000..90e046f
--- /dev/null
+++ b/src/ark/types/tenants/credential_retrieve_params.py
@@ -0,0 +1,16 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["CredentialRetrieveParams"]
+
+
+class CredentialRetrieveParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
+ reveal: bool
+ """Set to `true` to include the credential key in the response"""
diff --git a/src/ark/types/tenants/credential_retrieve_response.py b/src/ark/types/tenants/credential_retrieve_response.py
new file mode 100644
index 0000000..a3633e8
--- /dev/null
+++ b/src/ark/types/tenants/credential_retrieve_response.py
@@ -0,0 +1,63 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["CredentialRetrieveResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: int
+ """Unique identifier for the credential"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+ """When the credential was created"""
+
+ hold: bool
+ """
+ Whether the credential is on hold (disabled). When `true`, the credential cannot
+ be used to send emails.
+ """
+
+ last_used_at: Optional[datetime] = FieldInfo(alias="lastUsedAt", default=None)
+ """When the credential was last used to send an email"""
+
+ name: str
+ """Name of the credential"""
+
+ type: Literal["smtp", "api"]
+ """Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+ """
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+ """When the credential was last updated"""
+
+ key: Optional[str] = None
+ """The credential key (secret). Only included when:
+
+ - Creating a new credential (always returned)
+ - Retrieving with `reveal=true`
+ """
+
+ smtp_username: Optional[str] = FieldInfo(alias="smtpUsername", default=None)
+ """SMTP username for authentication.
+
+ Only included for SMTP credentials when the key is revealed.
+ """
+
+
+class CredentialRetrieveResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/tenants/credential_update_params.py b/src/ark/types/tenants/credential_update_params.py
new file mode 100644
index 0000000..f96a32c
--- /dev/null
+++ b/src/ark/types/tenants/credential_update_params.py
@@ -0,0 +1,22 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["CredentialUpdateParams"]
+
+
+class CredentialUpdateParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
+ hold: bool
+ """
+ Set to `true` to disable the credential (put on hold). Set to `false` to enable
+ the credential (release from hold).
+ """
+
+ name: str
+ """New name for the credential"""
diff --git a/src/ark/types/tenants/credential_update_response.py b/src/ark/types/tenants/credential_update_response.py
new file mode 100644
index 0000000..9b19b2f
--- /dev/null
+++ b/src/ark/types/tenants/credential_update_response.py
@@ -0,0 +1,63 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["CredentialUpdateResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: int
+ """Unique identifier for the credential"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+ """When the credential was created"""
+
+ hold: bool
+ """
+ Whether the credential is on hold (disabled). When `true`, the credential cannot
+ be used to send emails.
+ """
+
+ last_used_at: Optional[datetime] = FieldInfo(alias="lastUsedAt", default=None)
+ """When the credential was last used to send an email"""
+
+ name: str
+ """Name of the credential"""
+
+ type: Literal["smtp", "api"]
+ """Type of credential:
+
+ - `smtp` - For SMTP-based email sending
+ - `api` - For API-based email sending
+ """
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+ """When the credential was last updated"""
+
+ key: Optional[str] = None
+ """The credential key (secret). Only included when:
+
+ - Creating a new credential (always returned)
+ - Retrieving with `reveal=true`
+ """
+
+ smtp_username: Optional[str] = FieldInfo(alias="smtpUsername", default=None)
+ """SMTP username for authentication.
+
+ Only included for SMTP credentials when the key is revealed.
+ """
+
+
+class CredentialUpdateResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/tests/api_resources/tenants/__init__.py b/tests/api_resources/tenants/__init__.py
new file mode 100644
index 0000000..fd8019a
--- /dev/null
+++ b/tests/api_resources/tenants/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/tenants/test_credentials.py b/tests/api_resources/tenants/test_credentials.py
new file mode 100644
index 0000000..b919a73
--- /dev/null
+++ b/tests/api_resources/tenants/test_credentials.py
@@ -0,0 +1,509 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from ark import Ark, AsyncArk
+from tests.utils import assert_matches_type
+from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ark.types.tenants import (
+ CredentialListResponse,
+ CredentialCreateResponse,
+ CredentialDeleteResponse,
+ CredentialUpdateResponse,
+ CredentialRetrieveResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestCredentials:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Ark) -> None:
+ credential = client.tenants.credentials.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ )
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Ark) -> None:
+ response = client.tenants.credentials.with_raw_response.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = response.parse()
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Ark) -> None:
+ with client.tenants.credentials.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = response.parse()
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_create(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.credentials.with_raw_response.create(
+ tenant_id="",
+ name="production-smtp",
+ type="smtp",
+ )
+
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ credential = client.tenants.credentials.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Ark) -> None:
+ credential = client.tenants.credentials.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ reveal=True,
+ )
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.tenants.credentials.with_raw_response.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = response.parse()
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.tenants.credentials.with_streaming_response.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = response.parse()
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.credentials.with_raw_response.retrieve(
+ credential_id=123,
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_update(self, client: Ark) -> None:
+ credential = client.tenants.credentials.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ def test_method_update_with_all_params(self, client: Ark) -> None:
+ credential = client.tenants.credentials.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ hold=True,
+ name="production-smtp-v2",
+ )
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ def test_raw_response_update(self, client: Ark) -> None:
+ response = client.tenants.credentials.with_raw_response.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = response.parse()
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ def test_streaming_response_update(self, client: Ark) -> None:
+ with client.tenants.credentials.with_streaming_response.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = response.parse()
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_update(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.credentials.with_raw_response.update(
+ credential_id=123,
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_list(self, client: Ark) -> None:
+ credential = client.tenants.credentials.list(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(SyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ def test_method_list_with_all_params(self, client: Ark) -> None:
+ credential = client.tenants.credentials.list(
+ tenant_id="cm6abc123def456",
+ page=1,
+ per_page=1,
+ type="smtp",
+ )
+ assert_matches_type(SyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Ark) -> None:
+ response = client.tenants.credentials.with_raw_response.list(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = response.parse()
+ assert_matches_type(SyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Ark) -> None:
+ with client.tenants.credentials.with_streaming_response.list(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = response.parse()
+ assert_matches_type(SyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_list(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.credentials.with_raw_response.list(
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_delete(self, client: Ark) -> None:
+ credential = client.tenants.credentials.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: Ark) -> None:
+ response = client.tenants.credentials.with_raw_response.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = response.parse()
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: Ark) -> None:
+ with client.tenants.credentials.with_streaming_response.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = response.parse()
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.credentials.with_raw_response.delete(
+ credential_id=123,
+ tenant_id="",
+ )
+
+
+class TestAsyncCredentials:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ )
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.credentials.with_raw_response.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = await response.parse()
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.credentials.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
+ name="production-smtp",
+ type="smtp",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = await response.parse()
+ assert_matches_type(CredentialCreateResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.credentials.with_raw_response.create(
+ tenant_id="",
+ name="production-smtp",
+ type="smtp",
+ )
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ reveal=True,
+ )
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.credentials.with_raw_response.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = await response.parse()
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.credentials.with_streaming_response.retrieve(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = await response.parse()
+ assert_matches_type(CredentialRetrieveResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.credentials.with_raw_response.retrieve(
+ credential_id=123,
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_update(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ hold=True,
+ name="production-smtp-v2",
+ )
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_raw_response_update(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.credentials.with_raw_response.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = await response.parse()
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.credentials.with_streaming_response.update(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = await response.parse()
+ assert_matches_type(CredentialUpdateResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_update(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.credentials.with_raw_response.update(
+ credential_id=123,
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.list(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(AsyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.list(
+ tenant_id="cm6abc123def456",
+ page=1,
+ per_page=1,
+ type="smtp",
+ )
+ assert_matches_type(AsyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.credentials.with_raw_response.list(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.credentials.with_streaming_response.list(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[CredentialListResponse], credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.credentials.with_raw_response.list(
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncArk) -> None:
+ credential = await async_client.tenants.credentials.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.credentials.with_raw_response.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ credential = await response.parse()
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.credentials.with_streaming_response.delete(
+ credential_id=123,
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ credential = await response.parse()
+ assert_matches_type(CredentialDeleteResponse, credential, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.credentials.with_raw_response.delete(
+ credential_id=123,
+ tenant_id="",
+ )
diff --git a/tests/api_resources/test_domains.py b/tests/api_resources/test_domains.py
index 41dcfff..ff34324 100644
--- a/tests/api_resources/test_domains.py
+++ b/tests/api_resources/test_domains.py
@@ -27,6 +27,7 @@ class TestDomains:
def test_method_create(self, client: Ark) -> None:
domain = client.domains.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainCreateResponse, domain, path=["response"])
@@ -34,6 +35,7 @@ def test_method_create(self, client: Ark) -> None:
def test_raw_response_create(self, client: Ark) -> None:
response = client.domains.with_raw_response.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -45,6 +47,7 @@ def test_raw_response_create(self, client: Ark) -> None:
def test_streaming_response_create(self, client: Ark) -> None:
with client.domains.with_streaming_response.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -97,6 +100,13 @@ def test_method_list(self, client: Ark) -> None:
domain = client.domains.list()
assert_matches_type(DomainListResponse, domain, path=["response"])
+ @parametrize
+ def test_method_list_with_all_params(self, client: Ark) -> None:
+ domain = client.domains.list(
+ tenant_id="tenant_id",
+ )
+ assert_matches_type(DomainListResponse, domain, path=["response"])
+
@parametrize
def test_raw_response_list(self, client: Ark) -> None:
response = client.domains.with_raw_response.list()
@@ -203,6 +213,7 @@ class TestAsyncDomains:
async def test_method_create(self, async_client: AsyncArk) -> None:
domain = await async_client.domains.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainCreateResponse, domain, path=["response"])
@@ -210,6 +221,7 @@ async def test_method_create(self, async_client: AsyncArk) -> None:
async def test_raw_response_create(self, async_client: AsyncArk) -> None:
response = await async_client.domains.with_raw_response.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -221,6 +233,7 @@ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
async with async_client.domains.with_streaming_response.create(
name="notifications.myapp.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -273,6 +286,13 @@ async def test_method_list(self, async_client: AsyncArk) -> None:
domain = await async_client.domains.list()
assert_matches_type(DomainListResponse, domain, path=["response"])
+ @parametrize
+ async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None:
+ domain = await async_client.domains.list(
+ tenant_id="tenant_id",
+ )
+ assert_matches_type(DomainListResponse, domain, path=["response"])
+
@parametrize
async def test_raw_response_list(self, async_client: AsyncArk) -> None:
response = await async_client.domains.with_raw_response.list()
From 09a4d02d5acdb3b2a20ca62930e7c644bc5968c9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 21:25:19 +0000
Subject: [PATCH 2/6] feat(api): tenant usage
---
.stats.yml | 8 +-
api.md | 35 +-
src/ark/_client.py | 39 +-
src/ark/pagination.py | 108 ++-
src/ark/resources/__init__.py | 14 +
src/ark/resources/emails.py | 72 +-
src/ark/resources/limits.py | 171 ++++
src/ark/resources/usage.py | 733 +++++++++++++++++-
src/ark/types/__init__.py | 19 +
src/ark/types/bulk_tenant_usage.py | 78 ++
src/ark/types/email_counts.py | 27 +
src/ark/types/email_rates.py | 15 +
.../email_retrieve_deliveries_response.py | 2 +-
src/ark/types/limit_retrieve_response.py | 20 +
src/ark/types/limits_data.py | 97 +++
src/ark/types/tenant_usage.py | 32 +
src/ark/types/tenant_usage_timeseries.py | 54 ++
src/ark/types/usage_export_params.py | 24 +
src/ark/types/usage_export_response.py | 51 ++
src/ark/types/usage_list_by_tenant_params.py | 30 +
src/ark/types/usage_period.py | 17 +
src/ark/types/usage_retrieve_response.py | 95 +--
...usage_retrieve_tenant_timeseries_params.py | 18 +
...age_retrieve_tenant_timeseries_response.py | 20 +
.../usage_retrieve_tenant_usage_params.py | 24 +
.../usage_retrieve_tenant_usage_response.py | 20 +
tests/api_resources/test_emails.py | 32 +-
tests/api_resources/test_limits.py | 74 ++
tests/api_resources/test_usage.py | 373 ++++++++-
29 files changed, 2127 insertions(+), 175 deletions(-)
create mode 100644 src/ark/resources/limits.py
create mode 100644 src/ark/types/bulk_tenant_usage.py
create mode 100644 src/ark/types/email_counts.py
create mode 100644 src/ark/types/email_rates.py
create mode 100644 src/ark/types/limit_retrieve_response.py
create mode 100644 src/ark/types/limits_data.py
create mode 100644 src/ark/types/tenant_usage.py
create mode 100644 src/ark/types/tenant_usage_timeseries.py
create mode 100644 src/ark/types/usage_export_params.py
create mode 100644 src/ark/types/usage_export_response.py
create mode 100644 src/ark/types/usage_list_by_tenant_params.py
create mode 100644 src/ark/types/usage_period.py
create mode 100644 src/ark/types/usage_retrieve_tenant_timeseries_params.py
create mode 100644 src/ark/types/usage_retrieve_tenant_timeseries_response.py
create mode 100644 src/ark/types/usage_retrieve_tenant_usage_params.py
create mode 100644 src/ark/types/usage_retrieve_tenant_usage_response.py
create mode 100644 tests/api_resources/test_limits.py
diff --git a/.stats.yml b/.stats.yml
index 27d9c12..136030f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 45
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-3aa940f5978d27c19f21fd584c87674f7a1e2bf4495fbd9253e60e7b8e9d28ea.yml
-openapi_spec_hash: 7354939360962eb34b88189afbe97222
-config_hash: 7b825ebe29bcb14e63e718c041b8398c
+configured_endpoints: 50
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-80a061ee30021d968068b710420ca250a2a4cd6e9cec16f2ea5144afed2a6e33.yml
+openapi_spec_hash: 661c8877c1efd5c25f5ebc634e764a72
+config_hash: 569f48c6e4612e54e4db8a875be71e1a
diff --git a/api.md b/api.md
index 519ab85..69a6036 100644
--- a/api.md
+++ b/api.md
@@ -22,10 +22,10 @@ from ark.types import (
Methods:
-- client.emails.retrieve(id, \*\*params) -> EmailRetrieveResponse
+- client.emails.retrieve(email_id, \*\*params) -> EmailRetrieveResponse
- client.emails.list(\*\*params) -> SyncPageNumberPagination[EmailListResponse]
-- client.emails.retrieve_deliveries(id) -> EmailRetrieveDeliveriesResponse
-- client.emails.retry(id) -> EmailRetryResponse
+- client.emails.retrieve_deliveries(email_id) -> EmailRetrieveDeliveriesResponse
+- client.emails.retry(email_id) -> EmailRetryResponse
- client.emails.send(\*\*params) -> EmailSendResponse
- client.emails.send_batch(\*\*params) -> EmailSendBatchResponse
- client.emails.send_raw(\*\*params) -> EmailSendRawResponse
@@ -143,17 +143,44 @@ Methods:
- client.logs.retrieve(request_id) -> LogRetrieveResponse
- client.logs.list(\*\*params) -> SyncPageNumberPagination[LogEntry]
+# Limits
+
+Types:
+
+```python
+from ark.types import LimitsData, LimitRetrieveResponse
+```
+
+Methods:
+
+- client.limits.retrieve() -> LimitRetrieveResponse
+
# Usage
Types:
```python
-from ark.types import UsageRetrieveResponse
+from ark.types import (
+ BulkTenantUsage,
+ EmailCounts,
+ EmailRates,
+ TenantUsage,
+ TenantUsageTimeseries,
+ UsagePeriod,
+ UsageRetrieveResponse,
+ UsageExportResponse,
+ UsageRetrieveTenantTimeseriesResponse,
+ UsageRetrieveTenantUsageResponse,
+)
```
Methods:
- client.usage.retrieve() -> UsageRetrieveResponse
+- client.usage.export(\*\*params) -> UsageExportResponse
+- client.usage.list_by_tenant(\*\*params) -> SyncOffsetPagination[Tenant]
+- client.usage.retrieve_tenant_timeseries(tenant_id, \*\*params) -> UsageRetrieveTenantTimeseriesResponse
+- client.usage.retrieve_tenant_usage(tenant_id, \*\*params) -> UsageRetrieveTenantUsageResponse
# Tenants
diff --git a/src/ark/_client.py b/src/ark/_client.py
index 4958103..8ce241f 100644
--- a/src/ark/_client.py
+++ b/src/ark/_client.py
@@ -31,10 +31,11 @@
)
if TYPE_CHECKING:
- from .resources import logs, usage, emails, domains, tenants, tracking, webhooks, suppressions
+ from .resources import logs, usage, emails, limits, domains, tenants, tracking, webhooks, suppressions
from .resources.logs import LogsResource, AsyncLogsResource
from .resources.usage import UsageResource, AsyncUsageResource
from .resources.emails import EmailsResource, AsyncEmailsResource
+ from .resources.limits import LimitsResource, AsyncLimitsResource
from .resources.domains import DomainsResource, AsyncDomainsResource
from .resources.tracking import TrackingResource, AsyncTrackingResource
from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
@@ -135,6 +136,12 @@ def logs(self) -> LogsResource:
return LogsResource(self)
+ @cached_property
+ def limits(self) -> LimitsResource:
+ from .resources.limits import LimitsResource
+
+ return LimitsResource(self)
+
@cached_property
def usage(self) -> UsageResource:
from .resources.usage import UsageResource
@@ -351,6 +358,12 @@ def logs(self) -> AsyncLogsResource:
return AsyncLogsResource(self)
+ @cached_property
+ def limits(self) -> AsyncLimitsResource:
+ from .resources.limits import AsyncLimitsResource
+
+ return AsyncLimitsResource(self)
+
@cached_property
def usage(self) -> AsyncUsageResource:
from .resources.usage import AsyncUsageResource
@@ -518,6 +531,12 @@ def logs(self) -> logs.LogsResourceWithRawResponse:
return LogsResourceWithRawResponse(self._client.logs)
+ @cached_property
+ def limits(self) -> limits.LimitsResourceWithRawResponse:
+ from .resources.limits import LimitsResourceWithRawResponse
+
+ return LimitsResourceWithRawResponse(self._client.limits)
+
@cached_property
def usage(self) -> usage.UsageResourceWithRawResponse:
from .resources.usage import UsageResourceWithRawResponse
@@ -573,6 +592,12 @@ def logs(self) -> logs.AsyncLogsResourceWithRawResponse:
return AsyncLogsResourceWithRawResponse(self._client.logs)
+ @cached_property
+ def limits(self) -> limits.AsyncLimitsResourceWithRawResponse:
+ from .resources.limits import AsyncLimitsResourceWithRawResponse
+
+ return AsyncLimitsResourceWithRawResponse(self._client.limits)
+
@cached_property
def usage(self) -> usage.AsyncUsageResourceWithRawResponse:
from .resources.usage import AsyncUsageResourceWithRawResponse
@@ -628,6 +653,12 @@ def logs(self) -> logs.LogsResourceWithStreamingResponse:
return LogsResourceWithStreamingResponse(self._client.logs)
+ @cached_property
+ def limits(self) -> limits.LimitsResourceWithStreamingResponse:
+ from .resources.limits import LimitsResourceWithStreamingResponse
+
+ return LimitsResourceWithStreamingResponse(self._client.limits)
+
@cached_property
def usage(self) -> usage.UsageResourceWithStreamingResponse:
from .resources.usage import UsageResourceWithStreamingResponse
@@ -683,6 +714,12 @@ def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse:
return AsyncLogsResourceWithStreamingResponse(self._client.logs)
+ @cached_property
+ def limits(self) -> limits.AsyncLimitsResourceWithStreamingResponse:
+ from .resources.limits import AsyncLimitsResourceWithStreamingResponse
+
+ return AsyncLimitsResourceWithStreamingResponse(self._client.limits)
+
@cached_property
def usage(self) -> usage.AsyncUsageResourceWithStreamingResponse:
from .resources.usage import AsyncUsageResourceWithStreamingResponse
diff --git a/src/ark/pagination.py b/src/ark/pagination.py
index 234b226..6e0c4b0 100644
--- a/src/ark/pagination.py
+++ b/src/ark/pagination.py
@@ -5,10 +5,18 @@
from pydantic import Field as FieldInfo
-from ._models import BaseModel
+from ._models import BaseModel, GenericModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["PageNumberPaginationMeta", "SyncPageNumberPagination", "AsyncPageNumberPagination"]
+__all__ = [
+ "PageNumberPaginationMeta",
+ "SyncPageNumberPagination",
+ "AsyncPageNumberPagination",
+ "OffsetPaginationData",
+ "OffsetPaginationPagination",
+ "SyncOffsetPagination",
+ "AsyncOffsetPagination",
+]
_T = TypeVar("_T")
@@ -71,3 +79,99 @@ def next_page_info(self) -> Optional[PageInfo]:
return None
return PageInfo(params={"page": current_page + 1})
+
+
+class OffsetPaginationPagination(BaseModel):
+ has_more: Optional[bool] = None
+
+ limit: Optional[int] = None
+
+ offset: Optional[int] = None
+
+ total: Optional[int] = None
+
+
+class OffsetPaginationData(GenericModel, Generic[_T]):
+ pagination: Optional[OffsetPaginationPagination] = None
+
+ tenants: Optional[List[_T]] = None
+
+
+class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ data: Optional[OffsetPaginationData[_T]] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ tenants = None
+ if self.data is not None:
+ if self.data.tenants is not None:
+ tenants = self.data.tenants
+ if not tenants:
+ return []
+ return tenants
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ offset = None
+ if self.data is not None:
+ if self.data.pagination is not None:
+ if self.data.pagination.offset is not None:
+ offset = self.data.pagination.offset
+ if offset is None:
+ return None # type: ignore[unreachable]
+
+ length = len(self._get_page_items())
+ current_count = offset + length
+
+ total = None
+ if self.data is not None:
+ if self.data.pagination is not None:
+ if self.data.pagination.total is not None:
+ total = self.data.pagination.total
+ if total is None:
+ return None
+
+ if current_count < total:
+ return PageInfo(params={"offset": current_count})
+
+ return None
+
+
+class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ data: Optional[OffsetPaginationData[_T]] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ tenants = None
+ if self.data is not None:
+ if self.data.tenants is not None:
+ tenants = self.data.tenants
+ if not tenants:
+ return []
+ return tenants
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ offset = None
+ if self.data is not None:
+ if self.data.pagination is not None:
+ if self.data.pagination.offset is not None:
+ offset = self.data.pagination.offset
+ if offset is None:
+ return None # type: ignore[unreachable]
+
+ length = len(self._get_page_items())
+ current_count = offset + length
+
+ total = None
+ if self.data is not None:
+ if self.data.pagination is not None:
+ if self.data.pagination.total is not None:
+ total = self.data.pagination.total
+ if total is None:
+ return None
+
+ if current_count < total:
+ return PageInfo(params={"offset": current_count})
+
+ return None
diff --git a/src/ark/resources/__init__.py b/src/ark/resources/__init__.py
index f3812e4..e14e930 100644
--- a/src/ark/resources/__init__.py
+++ b/src/ark/resources/__init__.py
@@ -24,6 +24,14 @@
EmailsResourceWithStreamingResponse,
AsyncEmailsResourceWithStreamingResponse,
)
+from .limits import (
+ LimitsResource,
+ AsyncLimitsResource,
+ LimitsResourceWithRawResponse,
+ AsyncLimitsResourceWithRawResponse,
+ LimitsResourceWithStreamingResponse,
+ AsyncLimitsResourceWithStreamingResponse,
+)
from .domains import (
DomainsResource,
AsyncDomainsResource,
@@ -102,6 +110,12 @@
"AsyncLogsResourceWithRawResponse",
"LogsResourceWithStreamingResponse",
"AsyncLogsResourceWithStreamingResponse",
+ "LimitsResource",
+ "AsyncLimitsResource",
+ "LimitsResourceWithRawResponse",
+ "AsyncLimitsResourceWithRawResponse",
+ "LimitsResourceWithStreamingResponse",
+ "AsyncLimitsResourceWithStreamingResponse",
"UsageResource",
"AsyncUsageResource",
"UsageResourceWithRawResponse",
diff --git a/src/ark/resources/emails.py b/src/ark/resources/emails.py
index 4169d3a..77631ce 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,
- id: str,
+ email_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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return self._get(
- f"/emails/{id}",
+ f"/emails/{email_id}",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -137,7 +137,7 @@ def list(
**Related endpoints:**
- - `GET /emails/{id}` - Get full details of a specific email
+ - `GET /emails/{emailId}` - Get full details of a specific email
- `POST /emails` - Send a new email
Args:
@@ -200,7 +200,7 @@ def list(
def retrieve_deliveries(
self,
- id: str,
+ email_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/{id}/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/{emailId}/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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return self._get(
- f"/emails/{id}/deliveries",
+ f"/emails/{email_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,
- id: str,
+ email_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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return self._post(
- f"/emails/{id}/retry",
+ f"/emails/{email_id}/retry",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -339,9 +339,9 @@ def send(
**Related endpoints:**
- - `GET /emails/{id}` - Track delivery status
- - `GET /emails/{id}/deliveries` - View delivery attempts
- - `POST /emails/{id}/retry` - Retry failed delivery
+ - `GET /emails/{emailId}` - Track delivery status
+ - `GET /emails/{emailId}/deliveries` - View delivery attempts
+ - `POST /emails/{emailId}/retry` - Retry failed delivery
Args:
from_: Sender email address. Must be from a verified domain OR use sandbox mode.
@@ -575,7 +575,7 @@ def with_streaming_response(self) -> AsyncEmailsResourceWithStreamingResponse:
async def retrieve(
self,
- id: str,
+ email_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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return await self._get(
- f"/emails/{id}",
+ f"/emails/{email_id}",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -653,7 +653,7 @@ def list(
**Related endpoints:**
- - `GET /emails/{id}` - Get full details of a specific email
+ - `GET /emails/{emailId}` - Get full details of a specific email
- `POST /emails` - Send a new email
Args:
@@ -716,7 +716,7 @@ def list(
async def retrieve_deliveries(
self,
- id: str,
+ email_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/{id}/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/{emailId}/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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return await self._get(
- f"/emails/{id}/deliveries",
+ f"/emails/{email_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,
- id: str,
+ email_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 id:
- raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
+ if not email_id:
+ raise ValueError(f"Expected a non-empty value for `email_id` but received {email_id!r}")
return await self._post(
- f"/emails/{id}/retry",
+ f"/emails/{email_id}/retry",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -855,9 +855,9 @@ async def send(
**Related endpoints:**
- - `GET /emails/{id}` - Track delivery status
- - `GET /emails/{id}/deliveries` - View delivery attempts
- - `POST /emails/{id}/retry` - Retry failed delivery
+ - `GET /emails/{emailId}` - Track delivery status
+ - `GET /emails/{emailId}/deliveries` - View delivery attempts
+ - `POST /emails/{emailId}/retry` - Retry failed delivery
Args:
from_: Sender email address. Must be from a verified domain OR use sandbox mode.
diff --git a/src/ark/resources/limits.py b/src/ark/resources/limits.py
new file mode 100644
index 0000000..f18db42
--- /dev/null
+++ b/src/ark/resources/limits.py
@@ -0,0 +1,171 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from .._types import Body, Query, Headers, NotGiven, not_given
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+from ..types.limit_retrieve_response import LimitRetrieveResponse
+
+__all__ = ["LimitsResource", "AsyncLimitsResource"]
+
+
+class LimitsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> LimitsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return LimitsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> LimitsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return LimitsResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> LimitRetrieveResponse:
+ """
+ Returns current rate limit and send limit information for your account.
+
+ This endpoint is the recommended way to check your account's operational limits.
+ Use `/usage` endpoints for historical usage analytics.
+
+ **Response includes:**
+
+ - `rateLimit` - API request rate limit (requests per second)
+ - `sendLimit` - Email sending limit (emails per hour)
+ - `billing` - Credit balance and auto-recharge configuration
+
+ **Notes:**
+
+ - This request counts against your rate limit
+ - `sendLimit` may be null if Postal is temporarily unavailable
+ - `billing` is null if billing is not configured
+ - Send limit resets at the top of each hour
+ """
+ return self._get(
+ "/limits",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LimitRetrieveResponse,
+ )
+
+
+class AsyncLimitsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncLimitsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncLimitsResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ *,
+ # 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,
+ ) -> LimitRetrieveResponse:
+ """
+ Returns current rate limit and send limit information for your account.
+
+ This endpoint is the recommended way to check your account's operational limits.
+ Use `/usage` endpoints for historical usage analytics.
+
+ **Response includes:**
+
+ - `rateLimit` - API request rate limit (requests per second)
+ - `sendLimit` - Email sending limit (emails per hour)
+ - `billing` - Credit balance and auto-recharge configuration
+
+ **Notes:**
+
+ - This request counts against your rate limit
+ - `sendLimit` may be null if Postal is temporarily unavailable
+ - `billing` is null if billing is not configured
+ - Send limit resets at the top of each hour
+ """
+ return await self._get(
+ "/limits",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=LimitRetrieveResponse,
+ )
+
+
+class LimitsResourceWithRawResponse:
+ def __init__(self, limits: LimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = to_raw_response_wrapper(
+ limits.retrieve,
+ )
+
+
+class AsyncLimitsResourceWithRawResponse:
+ def __init__(self, limits: AsyncLimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = async_to_raw_response_wrapper(
+ limits.retrieve,
+ )
+
+
+class LimitsResourceWithStreamingResponse:
+ def __init__(self, limits: LimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = to_streamed_response_wrapper(
+ limits.retrieve,
+ )
+
+
+class AsyncLimitsResourceWithStreamingResponse:
+ def __init__(self, limits: AsyncLimitsResource) -> None:
+ self._limits = limits
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ limits.retrieve,
+ )
diff --git a/src/ark/resources/usage.py b/src/ark/resources/usage.py
index cb9004f..80acc02 100644
--- a/src/ark/resources/usage.py
+++ b/src/ark/resources/usage.py
@@ -2,9 +2,19 @@
from __future__ import annotations
+import typing_extensions
+from typing_extensions import Literal
+
import httpx
-from .._types import Body, Query, Headers, NotGiven, not_given
+from ..types import (
+ usage_export_params,
+ usage_list_by_tenant_params,
+ usage_retrieve_tenant_usage_params,
+ usage_retrieve_tenant_timeseries_params,
+)
+from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -13,8 +23,13 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from .._base_client import AsyncPaginator, make_request_options
+from ..types.bulk_tenant_usage import Tenant
+from ..types.usage_export_response import UsageExportResponse
from ..types.usage_retrieve_response import UsageRetrieveResponse
+from ..types.usage_retrieve_tenant_usage_response import UsageRetrieveTenantUsageResponse
+from ..types.usage_retrieve_tenant_timeseries_response import UsageRetrieveTenantTimeseriesResponse
__all__ = ["UsageResource", "AsyncUsageResource"]
@@ -39,6 +54,7 @@ def with_streaming_response(self) -> UsageResourceWithStreamingResponse:
"""
return UsageResourceWithStreamingResponse(self)
+ @typing_extensions.deprecated("deprecated")
def retrieve(
self,
*,
@@ -50,6 +66,9 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UsageRetrieveResponse:
"""
+ > **Deprecated:** Use `GET /limits` instead for rate limits and send limits.
+ > This endpoint will be removed in a future version.
+
Returns current usage and limit information for your account.
This endpoint is designed for:
@@ -70,6 +89,12 @@ def retrieve(
- `sendLimit` may be null if Postal is temporarily unavailable
- `billing` is null if billing is not configured
- Send limit resets at the top of each hour
+
+ **Migration:**
+
+ - For rate limits and send limits, use `GET /limits`
+ - For per-tenant usage analytics, use `GET /tenants/{tenantId}/usage`
+ - For bulk tenant usage, use `GET /usage/by-tenant`
"""
return self._get(
"/usage",
@@ -79,6 +104,317 @@ def retrieve(
cast_to=UsageRetrieveResponse,
)
+ def export(
+ self,
+ *,
+ format: Literal["csv", "jsonl", "json"] | Omit = omit,
+ min_sent: int | Omit = omit,
+ period: str | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageExportResponse:
+ """
+ Export usage data for all tenants in a format suitable for billing systems.
+
+ **Use cases:**
+
+ - Import into billing systems (Stripe, Chargebee, etc.)
+ - Generate invoices
+ - Archive usage data
+
+ **Export formats:**
+
+ - `csv` - Comma-separated values (default)
+ - `jsonl` - JSON Lines (one JSON object per line)
+ - `json` - JSON array
+
+ **Response headers:**
+
+ - `X-Total-Tenants` - Total number of tenants in export
+ - `X-Total-Sent` - Total emails sent across all tenants
+ - `Content-Disposition` - Suggested filename for download
+
+ This endpoint returns up to 10,000 tenants per request. For organizations with
+ more tenants, use the `/usage/by-tenant` endpoint with pagination.
+
+ Args:
+ format: Export format
+
+ min_sent: Only include tenants with at least this many emails sent
+
+ period: Time period for export. Defaults to current month.
+
+ status: Filter by tenant status
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ 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
+ """
+ return self._get(
+ "/usage/export",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "format": format,
+ "min_sent": min_sent,
+ "period": period,
+ "status": status,
+ "timezone": timezone,
+ },
+ usage_export_params.UsageExportParams,
+ ),
+ ),
+ cast_to=UsageExportResponse,
+ )
+
+ def list_by_tenant(
+ self,
+ *,
+ limit: int | Omit = omit,
+ min_sent: int | Omit = omit,
+ offset: int | Omit = omit,
+ period: str | Omit = omit,
+ sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
+ | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncOffsetPagination[Tenant]:
+ """
+ Returns email usage statistics for all tenants in your organization.
+
+ **Use cases:**
+
+ - Generate monthly billing reports
+ - Build admin dashboards showing all customer usage
+ - Identify high-volume or problematic tenants
+
+ **Sorting options:**
+
+ - `sent`, `-sent` - Sort by emails sent (ascending/descending)
+ - `delivered`, `-delivered` - Sort by emails delivered
+ - `bounce_rate`, `-bounce_rate` - Sort by bounce rate
+ - `name`, `-name` - Sort alphabetically by tenant name
+
+ **Filtering:**
+
+ - `status` - Filter by tenant status (active, suspended, archived)
+ - `min_sent` - Only include tenants with at least N emails sent
+
+ Results are paginated. Use `limit` and `offset` for pagination.
+
+ Args:
+ limit: Maximum number of tenants to return (1-100)
+
+ min_sent: Only include tenants with at least this many emails sent
+
+ offset: Number of tenants to skip for pagination
+
+ period: Time period for usage data. Defaults to current month.
+
+ sort: Sort order for results. Prefix with `-` for descending order.
+
+ status: Filter by tenant status
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ 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
+ """
+ return self._get_api_list(
+ "/usage/by-tenant",
+ page=SyncOffsetPagination[Tenant],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "min_sent": min_sent,
+ "offset": offset,
+ "period": period,
+ "sort": sort,
+ "status": status,
+ "timezone": timezone,
+ },
+ usage_list_by_tenant_params.UsageListByTenantParams,
+ ),
+ ),
+ model=Tenant,
+ )
+
+ def retrieve_tenant_timeseries(
+ self,
+ tenant_id: str,
+ *,
+ granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTenantTimeseriesResponse:
+ """
+ Returns time-bucketed email statistics for a specific tenant.
+
+ **Use cases:**
+
+ - Build usage charts and graphs
+ - Identify sending patterns
+ - Detect anomalies in delivery rates
+
+ **Granularity options:**
+
+ - `hour` - Hourly buckets (best for last 7 days)
+ - `day` - Daily buckets (best for last 30-90 days)
+ - `week` - Weekly buckets (best for last 6 months)
+ - `month` - Monthly buckets (best for year-over-year)
+
+ The response includes a data point for each time bucket with all email metrics.
+
+ Args:
+ granularity: Time bucket size for data points
+
+ period: Time period for timeseries data. Defaults to current month.
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/usage/timeseries",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "granularity": granularity,
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_tenant_timeseries_params.UsageRetrieveTenantTimeseriesParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTenantTimeseriesResponse,
+ )
+
+ def retrieve_tenant_usage(
+ self,
+ tenant_id: str,
+ *,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTenantUsageResponse:
+ """
+ Returns email sending statistics for a specific tenant over a time period.
+
+ **Use cases:**
+
+ - Display usage dashboard to your customers
+ - Calculate per-tenant billing
+ - Monitor tenant health and delivery rates
+
+ **Period formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01` (full month)
+ - Date range: `2024-01-01..2024-01-31`
+ - Single day: `2024-01-15`
+
+ **Response includes:**
+
+ - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
+ - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
+
+ Args:
+ period: Time period for usage data. Defaults to current month.
+
+ **Formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01`
+ - Range: `2024-01-01..2024-01-31`
+ - Day: `2024-01-15`
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/usage",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_tenant_usage_params.UsageRetrieveTenantUsageParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTenantUsageResponse,
+ )
+
class AsyncUsageResource(AsyncAPIResource):
@cached_property
@@ -100,6 +436,7 @@ def with_streaming_response(self) -> AsyncUsageResourceWithStreamingResponse:
"""
return AsyncUsageResourceWithStreamingResponse(self)
+ @typing_extensions.deprecated("deprecated")
async def retrieve(
self,
*,
@@ -111,6 +448,9 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UsageRetrieveResponse:
"""
+ > **Deprecated:** Use `GET /limits` instead for rate limits and send limits.
+ > This endpoint will be removed in a future version.
+
Returns current usage and limit information for your account.
This endpoint is designed for:
@@ -131,6 +471,12 @@ async def retrieve(
- `sendLimit` may be null if Postal is temporarily unavailable
- `billing` is null if billing is not configured
- Send limit resets at the top of each hour
+
+ **Migration:**
+
+ - For rate limits and send limits, use `GET /limits`
+ - For per-tenant usage analytics, use `GET /tenants/{tenantId}/usage`
+ - For bulk tenant usage, use `GET /usage/by-tenant`
"""
return await self._get(
"/usage",
@@ -140,13 +486,338 @@ async def retrieve(
cast_to=UsageRetrieveResponse,
)
+ async def export(
+ self,
+ *,
+ format: Literal["csv", "jsonl", "json"] | Omit = omit,
+ min_sent: int | Omit = omit,
+ period: str | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageExportResponse:
+ """
+ Export usage data for all tenants in a format suitable for billing systems.
+
+ **Use cases:**
+
+ - Import into billing systems (Stripe, Chargebee, etc.)
+ - Generate invoices
+ - Archive usage data
+
+ **Export formats:**
+
+ - `csv` - Comma-separated values (default)
+ - `jsonl` - JSON Lines (one JSON object per line)
+ - `json` - JSON array
+
+ **Response headers:**
+
+ - `X-Total-Tenants` - Total number of tenants in export
+ - `X-Total-Sent` - Total emails sent across all tenants
+ - `Content-Disposition` - Suggested filename for download
+
+ This endpoint returns up to 10,000 tenants per request. For organizations with
+ more tenants, use the `/usage/by-tenant` endpoint with pagination.
+
+ Args:
+ format: Export format
+
+ min_sent: Only include tenants with at least this many emails sent
+
+ period: Time period for export. Defaults to current month.
+
+ status: Filter by tenant status
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ 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
+ """
+ return await self._get(
+ "/usage/export",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "format": format,
+ "min_sent": min_sent,
+ "period": period,
+ "status": status,
+ "timezone": timezone,
+ },
+ usage_export_params.UsageExportParams,
+ ),
+ ),
+ cast_to=UsageExportResponse,
+ )
+
+ def list_by_tenant(
+ self,
+ *,
+ limit: int | Omit = omit,
+ min_sent: int | Omit = omit,
+ offset: int | Omit = omit,
+ period: str | Omit = omit,
+ sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
+ | Omit = omit,
+ status: Literal["active", "suspended", "archived"] | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[Tenant, AsyncOffsetPagination[Tenant]]:
+ """
+ Returns email usage statistics for all tenants in your organization.
+
+ **Use cases:**
+
+ - Generate monthly billing reports
+ - Build admin dashboards showing all customer usage
+ - Identify high-volume or problematic tenants
+
+ **Sorting options:**
+
+ - `sent`, `-sent` - Sort by emails sent (ascending/descending)
+ - `delivered`, `-delivered` - Sort by emails delivered
+ - `bounce_rate`, `-bounce_rate` - Sort by bounce rate
+ - `name`, `-name` - Sort alphabetically by tenant name
+
+ **Filtering:**
+
+ - `status` - Filter by tenant status (active, suspended, archived)
+ - `min_sent` - Only include tenants with at least N emails sent
+
+ Results are paginated. Use `limit` and `offset` for pagination.
+
+ Args:
+ limit: Maximum number of tenants to return (1-100)
+
+ min_sent: Only include tenants with at least this many emails sent
+
+ offset: Number of tenants to skip for pagination
+
+ period: Time period for usage data. Defaults to current month.
+
+ sort: Sort order for results. Prefix with `-` for descending order.
+
+ status: Filter by tenant status
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ 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
+ """
+ return self._get_api_list(
+ "/usage/by-tenant",
+ page=AsyncOffsetPagination[Tenant],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "limit": limit,
+ "min_sent": min_sent,
+ "offset": offset,
+ "period": period,
+ "sort": sort,
+ "status": status,
+ "timezone": timezone,
+ },
+ usage_list_by_tenant_params.UsageListByTenantParams,
+ ),
+ ),
+ model=Tenant,
+ )
+
+ async def retrieve_tenant_timeseries(
+ self,
+ tenant_id: str,
+ *,
+ granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTenantTimeseriesResponse:
+ """
+ Returns time-bucketed email statistics for a specific tenant.
+
+ **Use cases:**
+
+ - Build usage charts and graphs
+ - Identify sending patterns
+ - Detect anomalies in delivery rates
+
+ **Granularity options:**
+
+ - `hour` - Hourly buckets (best for last 7 days)
+ - `day` - Daily buckets (best for last 30-90 days)
+ - `week` - Weekly buckets (best for last 6 months)
+ - `month` - Monthly buckets (best for year-over-year)
+
+ The response includes a data point for each time bucket with all email metrics.
+
+ Args:
+ granularity: Time bucket size for data points
+
+ period: Time period for timeseries data. Defaults to current month.
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/usage/timeseries",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "granularity": granularity,
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_tenant_timeseries_params.UsageRetrieveTenantTimeseriesParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTenantTimeseriesResponse,
+ )
+
+ async def retrieve_tenant_usage(
+ self,
+ tenant_id: str,
+ *,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTenantUsageResponse:
+ """
+ Returns email sending statistics for a specific tenant over a time period.
+
+ **Use cases:**
+
+ - Display usage dashboard to your customers
+ - Calculate per-tenant billing
+ - Monitor tenant health and delivery rates
+
+ **Period formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01` (full month)
+ - Date range: `2024-01-01..2024-01-31`
+ - Single day: `2024-01-15`
+
+ **Response includes:**
+
+ - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
+ - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
+
+ Args:
+ period: Time period for usage data. Defaults to current month.
+
+ **Formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01`
+ - Range: `2024-01-01..2024-01-31`
+ - Day: `2024-01-15`
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/usage",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_tenant_usage_params.UsageRetrieveTenantUsageParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTenantUsageResponse,
+ )
+
class UsageResourceWithRawResponse:
def __init__(self, usage: UsageResource) -> None:
self._usage = usage
- self.retrieve = to_raw_response_wrapper(
- usage.retrieve,
+ self.retrieve = ( # pyright: ignore[reportDeprecated]
+ to_raw_response_wrapper(
+ usage.retrieve, # pyright: ignore[reportDeprecated],
+ )
+ )
+ self.export = to_raw_response_wrapper(
+ usage.export,
+ )
+ self.list_by_tenant = to_raw_response_wrapper(
+ usage.list_by_tenant,
+ )
+ self.retrieve_tenant_timeseries = to_raw_response_wrapper(
+ usage.retrieve_tenant_timeseries,
+ )
+ self.retrieve_tenant_usage = to_raw_response_wrapper(
+ usage.retrieve_tenant_usage,
)
@@ -154,8 +825,22 @@ class AsyncUsageResourceWithRawResponse:
def __init__(self, usage: AsyncUsageResource) -> None:
self._usage = usage
- self.retrieve = async_to_raw_response_wrapper(
- usage.retrieve,
+ self.retrieve = ( # pyright: ignore[reportDeprecated]
+ async_to_raw_response_wrapper(
+ usage.retrieve, # pyright: ignore[reportDeprecated],
+ )
+ )
+ self.export = async_to_raw_response_wrapper(
+ usage.export,
+ )
+ self.list_by_tenant = async_to_raw_response_wrapper(
+ usage.list_by_tenant,
+ )
+ self.retrieve_tenant_timeseries = async_to_raw_response_wrapper(
+ usage.retrieve_tenant_timeseries,
+ )
+ self.retrieve_tenant_usage = async_to_raw_response_wrapper(
+ usage.retrieve_tenant_usage,
)
@@ -163,8 +848,22 @@ class UsageResourceWithStreamingResponse:
def __init__(self, usage: UsageResource) -> None:
self._usage = usage
- self.retrieve = to_streamed_response_wrapper(
- usage.retrieve,
+ self.retrieve = ( # pyright: ignore[reportDeprecated]
+ to_streamed_response_wrapper(
+ usage.retrieve, # pyright: ignore[reportDeprecated],
+ )
+ )
+ self.export = to_streamed_response_wrapper(
+ usage.export,
+ )
+ self.list_by_tenant = to_streamed_response_wrapper(
+ usage.list_by_tenant,
+ )
+ self.retrieve_tenant_timeseries = to_streamed_response_wrapper(
+ usage.retrieve_tenant_timeseries,
+ )
+ self.retrieve_tenant_usage = to_streamed_response_wrapper(
+ usage.retrieve_tenant_usage,
)
@@ -172,6 +871,20 @@ class AsyncUsageResourceWithStreamingResponse:
def __init__(self, usage: AsyncUsageResource) -> None:
self._usage = usage
- self.retrieve = async_to_streamed_response_wrapper(
- usage.retrieve,
+ self.retrieve = ( # pyright: ignore[reportDeprecated]
+ async_to_streamed_response_wrapper(
+ usage.retrieve, # pyright: ignore[reportDeprecated],
+ )
+ )
+ self.export = async_to_streamed_response_wrapper(
+ usage.export,
+ )
+ self.list_by_tenant = async_to_streamed_response_wrapper(
+ usage.list_by_tenant,
+ )
+ self.retrieve_tenant_timeseries = async_to_streamed_response_wrapper(
+ usage.retrieve_tenant_timeseries,
+ )
+ self.retrieve_tenant_usage = async_to_streamed_response_wrapper(
+ usage.retrieve_tenant_usage,
)
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index 42467e4..f53297c 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -6,15 +6,22 @@
from .tenant import Tenant as Tenant
from .log_entry import LogEntry as LogEntry
from .dns_record import DNSRecord as DNSRecord
+from .email_rates import EmailRates as EmailRates
+from .limits_data import LimitsData as LimitsData
+from .email_counts import EmailCounts as EmailCounts
+from .tenant_usage import TenantUsage as TenantUsage
from .track_domain import TrackDomain as TrackDomain
+from .usage_period import UsagePeriod as UsagePeriod
from .log_list_params import LogListParams as LogListParams
from .log_entry_detail import LogEntryDetail as LogEntryDetail
+from .bulk_tenant_usage import BulkTenantUsage as BulkTenantUsage
from .email_list_params import EmailListParams as EmailListParams
from .email_send_params import EmailSendParams as EmailSendParams
from .domain_list_params import DomainListParams as DomainListParams
from .tenant_list_params import TenantListParams as TenantListParams
from .email_list_response import EmailListResponse as EmailListResponse
from .email_send_response import EmailSendResponse as EmailSendResponse
+from .usage_export_params import UsageExportParams as UsageExportParams
from .webhook_test_params import WebhookTestParams as WebhookTestParams
from .domain_create_params import DomainCreateParams as DomainCreateParams
from .domain_list_response import DomainListResponse as DomainListResponse
@@ -24,6 +31,7 @@
from .email_retrieve_params import EmailRetrieveParams as EmailRetrieveParams
from .email_send_raw_params import EmailSendRawParams as EmailSendRawParams
from .log_retrieve_response import LogRetrieveResponse as LogRetrieveResponse
+from .usage_export_response import UsageExportResponse as UsageExportResponse
from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
from .webhook_list_response import WebhookListResponse as WebhookListResponse
from .webhook_test_response import WebhookTestResponse as WebhookTestResponse
@@ -40,7 +48,9 @@
from .email_retrieve_response import EmailRetrieveResponse as EmailRetrieveResponse
from .email_send_batch_params import EmailSendBatchParams as EmailSendBatchParams
from .email_send_raw_response import EmailSendRawResponse as EmailSendRawResponse
+from .limit_retrieve_response import LimitRetrieveResponse as LimitRetrieveResponse
from .suppression_list_params import SuppressionListParams as SuppressionListParams
+from .tenant_usage_timeseries import TenantUsageTimeseries as TenantUsageTimeseries
from .usage_retrieve_response import UsageRetrieveResponse as UsageRetrieveResponse
from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse
from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
@@ -58,6 +68,7 @@
from .tracking_retrieve_response import TrackingRetrieveResponse as TrackingRetrieveResponse
from .suppression_create_response import SuppressionCreateResponse as SuppressionCreateResponse
from .suppression_delete_response import SuppressionDeleteResponse as SuppressionDeleteResponse
+from .usage_list_by_tenant_params import UsageListByTenantParams as UsageListByTenantParams
from .suppression_retrieve_response import SuppressionRetrieveResponse as SuppressionRetrieveResponse
from .suppression_bulk_create_params import SuppressionBulkCreateParams as SuppressionBulkCreateParams
from .webhook_list_deliveries_params import WebhookListDeliveriesParams as WebhookListDeliveriesParams
@@ -65,4 +76,12 @@
from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
from .email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse as EmailRetrieveDeliveriesResponse
+from .usage_retrieve_tenant_usage_params import UsageRetrieveTenantUsageParams as UsageRetrieveTenantUsageParams
from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
+from .usage_retrieve_tenant_usage_response import UsageRetrieveTenantUsageResponse as UsageRetrieveTenantUsageResponse
+from .usage_retrieve_tenant_timeseries_params import (
+ UsageRetrieveTenantTimeseriesParams as UsageRetrieveTenantTimeseriesParams,
+)
+from .usage_retrieve_tenant_timeseries_response import (
+ UsageRetrieveTenantTimeseriesResponse as UsageRetrieveTenantTimeseriesResponse,
+)
diff --git a/src/ark/types/bulk_tenant_usage.py b/src/ark/types/bulk_tenant_usage.py
new file mode 100644
index 0000000..eedef65
--- /dev/null
+++ b/src/ark/types/bulk_tenant_usage.py
@@ -0,0 +1,78 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .email_rates import EmailRates
+from .email_counts import EmailCounts
+from .usage_period import UsagePeriod
+
+__all__ = ["BulkTenantUsage", "Pagination", "Summary", "Tenant"]
+
+
+class Pagination(BaseModel):
+ """Pagination information for usage queries"""
+
+ has_more: bool
+ """Whether more pages are available"""
+
+ limit: int
+ """Maximum items per page"""
+
+ offset: int
+ """Number of items skipped"""
+
+ total: int
+ """Total number of tenants matching the query"""
+
+
+class Summary(BaseModel):
+ """Aggregate summary across all tenants"""
+
+ total_delivered: int
+ """Total emails delivered across all tenants"""
+
+ total_sent: int
+ """Total emails sent across all tenants"""
+
+ total_tenants: int
+ """Total number of tenants in the query"""
+
+
+class Tenant(BaseModel):
+ """Usage record for a single tenant in bulk response"""
+
+ emails: EmailCounts
+ """Email delivery counts"""
+
+ rates: EmailRates
+ """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Current tenant status"""
+
+ tenant_id: str
+ """Unique tenant identifier"""
+
+ tenant_name: str
+ """Tenant display name"""
+
+ external_id: Optional[str] = None
+ """Your external ID for this tenant"""
+
+
+class BulkTenantUsage(BaseModel):
+ """Bulk tenant usage data with pagination"""
+
+ pagination: Pagination
+ """Pagination information for usage queries"""
+
+ period: UsagePeriod
+ """Time period for usage data"""
+
+ summary: Summary
+ """Aggregate summary across all tenants"""
+
+ tenants: List[Tenant]
+ """Array of tenant usage records"""
diff --git a/src/ark/types/email_counts.py b/src/ark/types/email_counts.py
new file mode 100644
index 0000000..ffd5cf6
--- /dev/null
+++ b/src/ark/types/email_counts.py
@@ -0,0 +1,27 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["EmailCounts"]
+
+
+class EmailCounts(BaseModel):
+ """Email delivery counts"""
+
+ bounced: int
+ """Emails that bounced"""
+
+ delivered: int
+ """Emails successfully delivered"""
+
+ hard_failed: int
+ """Emails that hard-failed (permanent failures)"""
+
+ held: int
+ """Emails currently held for review"""
+
+ sent: int
+ """Total emails sent"""
+
+ soft_failed: int
+ """Emails that soft-failed (temporary failures, may be retried)"""
diff --git a/src/ark/types/email_rates.py b/src/ark/types/email_rates.py
new file mode 100644
index 0000000..79450da
--- /dev/null
+++ b/src/ark/types/email_rates.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["EmailRates"]
+
+
+class EmailRates(BaseModel):
+ """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
+
+ bounce_rate: float
+ """Percentage of sent emails that bounced (0-1)"""
+
+ delivery_rate: float
+ """Percentage of sent emails that were delivered (0-1)"""
diff --git a/src/ark/types/email_retrieve_deliveries_response.py b/src/ark/types/email_retrieve_deliveries_response.py
index a591b9d..a923d98 100644
--- a/src/ark/types/email_retrieve_deliveries_response.py
+++ b/src/ark/types/email_retrieve_deliveries_response.py
@@ -142,7 +142,7 @@ class Data(BaseModel):
can_retry_manually: bool = FieldInfo(alias="canRetryManually")
"""
- Whether the message can be manually retried via `POST /emails/{id}/retry`.
+ Whether the message can be manually retried via `POST /emails/{emailId}/retry`.
`true` when the raw message content is still available (not expired). Messages
older than the retention period cannot be retried.
"""
diff --git a/src/ark/types/limit_retrieve_response.py b/src/ark/types/limit_retrieve_response.py
new file mode 100644
index 0000000..06bd330
--- /dev/null
+++ b/src/ark/types/limit_retrieve_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .limits_data import LimitsData
+from .shared.api_meta import APIMeta
+
+__all__ = ["LimitRetrieveResponse"]
+
+
+class LimitRetrieveResponse(BaseModel):
+ """Account rate limits and send limits response"""
+
+ data: LimitsData
+ """Current usage and limit information"""
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/limits_data.py b/src/ark/types/limits_data.py
new file mode 100644
index 0000000..015054c
--- /dev/null
+++ b/src/ark/types/limits_data.py
@@ -0,0 +1,97 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+
+__all__ = ["LimitsData", "Billing", "BillingAutoRecharge", "RateLimit", "SendLimit"]
+
+
+class BillingAutoRecharge(BaseModel):
+ """Auto-recharge configuration"""
+
+ amount: str
+ """Amount to recharge when triggered"""
+
+ enabled: bool
+ """Whether auto-recharge is enabled"""
+
+ threshold: str
+ """Balance threshold that triggers recharge"""
+
+
+class Billing(BaseModel):
+ """Billing and credit information"""
+
+ auto_recharge: BillingAutoRecharge = FieldInfo(alias="autoRecharge")
+ """Auto-recharge configuration"""
+
+ credit_balance: str = FieldInfo(alias="creditBalance")
+ """Current credit balance as formatted string (e.g., "25.50")"""
+
+ credit_balance_cents: int = FieldInfo(alias="creditBalanceCents")
+ """Current credit balance in cents for precise calculations"""
+
+ has_payment_method: bool = FieldInfo(alias="hasPaymentMethod")
+ """Whether a payment method is configured"""
+
+
+class RateLimit(BaseModel):
+ """API rate limit status"""
+
+ limit: int
+ """Maximum requests allowed per period"""
+
+ period: Literal["second"]
+ """Time period for the limit"""
+
+ remaining: int
+ """Requests remaining in current window"""
+
+ reset: int
+ """Unix timestamp when the limit resets"""
+
+
+class SendLimit(BaseModel):
+ """Email send limit status (hourly cap)"""
+
+ approaching: bool
+ """Whether approaching the limit (>90%)"""
+
+ exceeded: bool
+ """Whether the limit has been exceeded"""
+
+ limit: Optional[int] = None
+ """Maximum emails allowed per hour (null = unlimited)"""
+
+ period: Literal["hour"]
+ """Time period for the limit"""
+
+ remaining: Optional[int] = None
+ """Emails remaining in current period (null if unlimited)"""
+
+ resets_at: datetime = FieldInfo(alias="resetsAt")
+ """ISO timestamp when the limit window resets (top of next hour)"""
+
+ usage_percent: Optional[float] = FieldInfo(alias="usagePercent", default=None)
+ """Usage as a percentage (null if unlimited)"""
+
+ used: int
+ """Emails sent in current period"""
+
+
+class LimitsData(BaseModel):
+ """Current usage and limit information"""
+
+ billing: Optional[Billing] = None
+ """Billing and credit information"""
+
+ rate_limit: RateLimit = FieldInfo(alias="rateLimit")
+ """API rate limit status"""
+
+ send_limit: Optional[SendLimit] = FieldInfo(alias="sendLimit", default=None)
+ """Email send limit status (hourly cap)"""
diff --git a/src/ark/types/tenant_usage.py b/src/ark/types/tenant_usage.py
new file mode 100644
index 0000000..690a82a
--- /dev/null
+++ b/src/ark/types/tenant_usage.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+
+from .._models import BaseModel
+from .email_rates import EmailRates
+from .email_counts import EmailCounts
+from .usage_period import UsagePeriod
+
+__all__ = ["TenantUsage"]
+
+
+class TenantUsage(BaseModel):
+ """Tenant usage statistics"""
+
+ emails: EmailCounts
+ """Email delivery counts"""
+
+ period: UsagePeriod
+ """Time period for usage data"""
+
+ rates: EmailRates
+ """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
+
+ tenant_id: str
+ """Unique tenant identifier"""
+
+ tenant_name: str
+ """Tenant display name"""
+
+ external_id: Optional[str] = None
+ """Your external ID for this tenant (from metadata)"""
diff --git a/src/ark/types/tenant_usage_timeseries.py b/src/ark/types/tenant_usage_timeseries.py
new file mode 100644
index 0000000..2f71fc1
--- /dev/null
+++ b/src/ark/types/tenant_usage_timeseries.py
@@ -0,0 +1,54 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from datetime import datetime
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .usage_period import UsagePeriod
+
+__all__ = ["TenantUsageTimeseries", "Data"]
+
+
+class Data(BaseModel):
+ """Single timeseries data point"""
+
+ bounced: int
+ """Bounces in this bucket"""
+
+ delivered: int
+ """Emails delivered in this bucket"""
+
+ hard_failed: int
+ """Hard failures in this bucket"""
+
+ held: int
+ """Emails held in this bucket"""
+
+ sent: int
+ """Emails sent in this bucket"""
+
+ soft_failed: int
+ """Soft failures in this bucket"""
+
+ timestamp: datetime
+ """Start of time bucket"""
+
+
+class TenantUsageTimeseries(BaseModel):
+ """Timeseries usage statistics"""
+
+ data: List[Data]
+ """Array of time-bucketed data points"""
+
+ granularity: Literal["hour", "day", "week", "month"]
+ """Time bucket granularity"""
+
+ period: UsagePeriod
+ """Time period for usage data"""
+
+ tenant_id: str
+ """Unique tenant identifier"""
+
+ tenant_name: str
+ """Tenant display name"""
diff --git a/src/ark/types/usage_export_params.py b/src/ark/types/usage_export_params.py
new file mode 100644
index 0000000..a809124
--- /dev/null
+++ b/src/ark/types/usage_export_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["UsageExportParams"]
+
+
+class UsageExportParams(TypedDict, total=False):
+ format: Literal["csv", "jsonl", "json"]
+ """Export format"""
+
+ min_sent: int
+ """Only include tenants with at least this many emails sent"""
+
+ period: str
+ """Time period for export. Defaults to current month."""
+
+ status: Literal["active", "suspended", "archived"]
+ """Filter by tenant status"""
+
+ timezone: str
+ """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_export_response.py b/src/ark/types/usage_export_response.py
new file mode 100644
index 0000000..3680f66
--- /dev/null
+++ b/src/ark/types/usage_export_response.py
@@ -0,0 +1,51 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal, TypeAlias
+
+from .._models import BaseModel
+
+__all__ = ["UsageExportResponse", "UsageExportResponseItem"]
+
+
+class UsageExportResponseItem(BaseModel):
+ """Single row in usage export (JSON format)"""
+
+ bounce_rate: float
+ """Bounce rate (0-1)"""
+
+ bounced: int
+ """Emails that bounced"""
+
+ delivered: int
+ """Emails successfully delivered"""
+
+ delivery_rate: float
+ """Delivery rate (0-1)"""
+
+ hard_failed: int
+ """Emails that hard-failed"""
+
+ held: int
+ """Emails currently held"""
+
+ sent: int
+ """Total emails sent"""
+
+ soft_failed: int
+ """Emails that soft-failed"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Current tenant status"""
+
+ tenant_id: str
+ """Unique tenant identifier"""
+
+ tenant_name: str
+ """Tenant display name"""
+
+ external_id: Optional[str] = None
+ """Your external ID for this tenant"""
+
+
+UsageExportResponse: TypeAlias = List[UsageExportResponseItem]
diff --git a/src/ark/types/usage_list_by_tenant_params.py b/src/ark/types/usage_list_by_tenant_params.py
new file mode 100644
index 0000000..0956398
--- /dev/null
+++ b/src/ark/types/usage_list_by_tenant_params.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["UsageListByTenantParams"]
+
+
+class UsageListByTenantParams(TypedDict, total=False):
+ limit: int
+ """Maximum number of tenants to return (1-100)"""
+
+ min_sent: int
+ """Only include tenants with at least this many emails sent"""
+
+ offset: int
+ """Number of tenants to skip for pagination"""
+
+ period: str
+ """Time period for usage data. Defaults to current month."""
+
+ sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
+ """Sort order for results. Prefix with `-` for descending order."""
+
+ status: Literal["active", "suspended", "archived"]
+ """Filter by tenant status"""
+
+ timezone: str
+ """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_period.py b/src/ark/types/usage_period.py
new file mode 100644
index 0000000..4372295
--- /dev/null
+++ b/src/ark/types/usage_period.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+
+from .._models import BaseModel
+
+__all__ = ["UsagePeriod"]
+
+
+class UsagePeriod(BaseModel):
+ """Time period for usage data"""
+
+ end: datetime
+ """Period end (inclusive)"""
+
+ start: datetime
+ """Period start (inclusive)"""
diff --git a/src/ark/types/usage_retrieve_response.py b/src/ark/types/usage_retrieve_response.py
index 86daa16..ce54e61 100644
--- a/src/ark/types/usage_retrieve_response.py
+++ b/src/ark/types/usage_retrieve_response.py
@@ -1,107 +1,18 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Optional
-from datetime import datetime
from typing_extensions import Literal
-from pydantic import Field as FieldInfo
-
from .._models import BaseModel
+from .limits_data import LimitsData
from .shared.api_meta import APIMeta
-__all__ = ["UsageRetrieveResponse", "Data", "DataBilling", "DataBillingAutoRecharge", "DataRateLimit", "DataSendLimit"]
-
-
-class DataBillingAutoRecharge(BaseModel):
- """Auto-recharge configuration"""
-
- amount: str
- """Amount to recharge when triggered"""
-
- enabled: bool
- """Whether auto-recharge is enabled"""
-
- threshold: str
- """Balance threshold that triggers recharge"""
-
-
-class DataBilling(BaseModel):
- """Billing and credit information"""
-
- auto_recharge: DataBillingAutoRecharge = FieldInfo(alias="autoRecharge")
- """Auto-recharge configuration"""
-
- credit_balance: str = FieldInfo(alias="creditBalance")
- """Current credit balance as formatted string (e.g., "25.50")"""
-
- credit_balance_cents: int = FieldInfo(alias="creditBalanceCents")
- """Current credit balance in cents for precise calculations"""
-
- has_payment_method: bool = FieldInfo(alias="hasPaymentMethod")
- """Whether a payment method is configured"""
-
-
-class DataRateLimit(BaseModel):
- """API rate limit status"""
-
- limit: int
- """Maximum requests allowed per period"""
-
- period: Literal["second"]
- """Time period for the limit"""
-
- remaining: int
- """Requests remaining in current window"""
-
- reset: int
- """Unix timestamp when the limit resets"""
-
-
-class DataSendLimit(BaseModel):
- """Email send limit status (hourly cap)"""
-
- approaching: bool
- """Whether approaching the limit (>90%)"""
-
- exceeded: bool
- """Whether the limit has been exceeded"""
-
- limit: Optional[int] = None
- """Maximum emails allowed per hour (null = unlimited)"""
-
- period: Literal["hour"]
- """Time period for the limit"""
-
- remaining: Optional[int] = None
- """Emails remaining in current period (null if unlimited)"""
-
- resets_at: datetime = FieldInfo(alias="resetsAt")
- """ISO timestamp when the limit window resets (top of next hour)"""
-
- usage_percent: Optional[float] = FieldInfo(alias="usagePercent", default=None)
- """Usage as a percentage (null if unlimited)"""
-
- used: int
- """Emails sent in current period"""
-
-
-class Data(BaseModel):
- """Current usage and limit information"""
-
- billing: Optional[DataBilling] = None
- """Billing and credit information"""
-
- rate_limit: DataRateLimit = FieldInfo(alias="rateLimit")
- """API rate limit status"""
-
- send_limit: Optional[DataSendLimit] = FieldInfo(alias="sendLimit", default=None)
- """Email send limit status (hourly cap)"""
+__all__ = ["UsageRetrieveResponse"]
class UsageRetrieveResponse(BaseModel):
"""Account usage and limits response"""
- data: Data
+ data: LimitsData
"""Current usage and limit information"""
meta: APIMeta
diff --git a/src/ark/types/usage_retrieve_tenant_timeseries_params.py b/src/ark/types/usage_retrieve_tenant_timeseries_params.py
new file mode 100644
index 0000000..59952b7
--- /dev/null
+++ b/src/ark/types/usage_retrieve_tenant_timeseries_params.py
@@ -0,0 +1,18 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["UsageRetrieveTenantTimeseriesParams"]
+
+
+class UsageRetrieveTenantTimeseriesParams(TypedDict, total=False):
+ granularity: Literal["hour", "day", "week", "month"]
+ """Time bucket size for data points"""
+
+ period: str
+ """Time period for timeseries data. Defaults to current month."""
+
+ timezone: str
+ """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_retrieve_tenant_timeseries_response.py b/src/ark/types/usage_retrieve_tenant_timeseries_response.py
new file mode 100644
index 0000000..2199ee5
--- /dev/null
+++ b/src/ark/types/usage_retrieve_tenant_timeseries_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .shared.api_meta import APIMeta
+from .tenant_usage_timeseries import TenantUsageTimeseries
+
+__all__ = ["UsageRetrieveTenantTimeseriesResponse"]
+
+
+class UsageRetrieveTenantTimeseriesResponse(BaseModel):
+ """Timeseries usage data for a tenant"""
+
+ data: TenantUsageTimeseries
+ """Timeseries usage statistics"""
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/usage_retrieve_tenant_usage_params.py b/src/ark/types/usage_retrieve_tenant_usage_params.py
new file mode 100644
index 0000000..d6b339f
--- /dev/null
+++ b/src/ark/types/usage_retrieve_tenant_usage_params.py
@@ -0,0 +1,24 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["UsageRetrieveTenantUsageParams"]
+
+
+class UsageRetrieveTenantUsageParams(TypedDict, total=False):
+ period: str
+ """Time period for usage data. Defaults to current month.
+
+ **Formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01`
+ - Range: `2024-01-01..2024-01-31`
+ - Day: `2024-01-15`
+ """
+
+ timezone: str
+ """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_retrieve_tenant_usage_response.py b/src/ark/types/usage_retrieve_tenant_usage_response.py
new file mode 100644
index 0000000..e528001
--- /dev/null
+++ b/src/ark/types/usage_retrieve_tenant_usage_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+from .tenant_usage import TenantUsage
+from .shared.api_meta import APIMeta
+
+__all__ = ["UsageRetrieveTenantUsageResponse"]
+
+
+class UsageRetrieveTenantUsageResponse(BaseModel):
+ """Usage statistics for a single tenant"""
+
+ data: TenantUsage
+ """Tenant usage statistics"""
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/tests/api_resources/test_emails.py b/tests/api_resources/test_emails.py
index 7a54df7..0e37b72 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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"):
client.emails.with_raw_response.retrieve(
- id="",
+ email_id="",
)
@parametrize
@@ -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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"):
client.emails.with_raw_response.retrieve_deliveries(
"",
)
@@ -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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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(
- id="aBc123XyZ",
+ email_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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"):
await async_client.emails.with_raw_response.retrieve(
- id="",
+ email_id="",
)
@parametrize
@@ -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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"):
await async_client.emails.with_raw_response.retrieve_deliveries(
"",
)
@@ -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 `id` but received ''"):
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email_id` but received ''"):
await async_client.emails.with_raw_response.retry(
"",
)
diff --git a/tests/api_resources/test_limits.py b/tests/api_resources/test_limits.py
new file mode 100644
index 0000000..f303d24
--- /dev/null
+++ b/tests/api_resources/test_limits.py
@@ -0,0 +1,74 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from ark import Ark, AsyncArk
+from ark.types import LimitRetrieveResponse
+from tests.utils import assert_matches_type
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestLimits:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ limit = client.limits.retrieve()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.limits.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = response.parse()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.limits.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = response.parse()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncLimits:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ limit = await async_client.limits.retrieve()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.limits.with_raw_response.retrieve()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ limit = await response.parse()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.limits.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ limit = await response.parse()
+ assert_matches_type(LimitRetrieveResponse, limit, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_usage.py b/tests/api_resources/test_usage.py
index 767da80..a98b600 100644
--- a/tests/api_resources/test_usage.py
+++ b/tests/api_resources/test_usage.py
@@ -8,8 +8,17 @@
import pytest
from ark import Ark, AsyncArk
-from ark.types import UsageRetrieveResponse
+from ark.types import (
+ UsageExportResponse,
+ UsageRetrieveResponse,
+ UsageRetrieveTenantUsageResponse,
+ UsageRetrieveTenantTimeseriesResponse,
+)
from tests.utils import assert_matches_type
+from ark.pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ark.types.bulk_tenant_usage import Tenant
+
+# pyright: reportDeprecated=false
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -19,12 +28,15 @@ class TestUsage:
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- usage = client.usage.retrieve()
+ with pytest.warns(DeprecationWarning):
+ usage = client.usage.retrieve()
+
assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.usage.with_raw_response.retrieve()
+ with pytest.warns(DeprecationWarning):
+ response = client.usage.with_raw_response.retrieve()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -33,15 +45,185 @@ def test_raw_response_retrieve(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.usage.with_streaming_response.retrieve() as response:
+ with pytest.warns(DeprecationWarning):
+ with client.usage.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_export(self, client: Ark) -> None:
+ usage = client.usage.export()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ def test_method_export_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.export(
+ format="csv",
+ min_sent=0,
+ period="period",
+ status="active",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_export(self, client: Ark) -> None:
+ response = client.usage.with_raw_response.export()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_export(self, client: Ark) -> None:
+ with client.usage.with_streaming_response.export() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_list_by_tenant(self, client: Ark) -> None:
+ usage = client.usage.list_by_tenant()
+ assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ def test_method_list_by_tenant_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.list_by_tenant(
+ limit=1,
+ min_sent=0,
+ offset=0,
+ period="period",
+ sort="sent",
+ status="active",
+ timezone="timezone",
+ )
+ assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_list_by_tenant(self, client: Ark) -> None:
+ response = client.usage.with_raw_response.list_by_tenant()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list_by_tenant(self, client: Ark) -> None:
+ with client.usage.with_streaming_response.list_by_tenant() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_retrieve_tenant_timeseries(self, client: Ark) -> None:
+ usage = client.usage.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_tenant_timeseries_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ granularity="hour",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_tenant_timeseries(self, client: Ark) -> None:
+ response = client.usage.with_raw_response.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_tenant_timeseries(self, client: Ark) -> None:
+ with client.usage.with_streaming_response.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_tenant_timeseries(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.usage.with_raw_response.retrieve_tenant_timeseries(
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_retrieve_tenant_usage(self, client: Ark) -> None:
+ usage = client.usage.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_tenant_usage_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_tenant_usage(self, client: Ark) -> None:
+ response = client.usage.with_raw_response.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_tenant_usage(self, client: Ark) -> None:
+ with client.usage.with_streaming_response.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_tenant_usage(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.usage.with_raw_response.retrieve_tenant_usage(
+ tenant_id="",
+ )
+
class TestAsyncUsage:
parametrize = pytest.mark.parametrize(
@@ -50,12 +232,15 @@ class TestAsyncUsage:
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.retrieve()
+ with pytest.warns(DeprecationWarning):
+ usage = await async_client.usage.retrieve()
+
assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.usage.with_raw_response.retrieve()
+ with pytest.warns(DeprecationWarning):
+ response = await async_client.usage.with_raw_response.retrieve()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -64,11 +249,181 @@ 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.usage.with_streaming_response.retrieve() as response:
+ with pytest.warns(DeprecationWarning):
+ async with async_client.usage.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_export(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.export()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_method_export_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.export(
+ format="csv",
+ min_sent=0,
+ period="period",
+ status="active",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_export(self, async_client: AsyncArk) -> None:
+ response = await async_client.usage.with_raw_response.export()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_export(self, async_client: AsyncArk) -> None:
+ async with async_client.usage.with_streaming_response.export() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(UsageExportResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_list_by_tenant(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.list_by_tenant()
+ assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ async def test_method_list_by_tenant_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.list_by_tenant(
+ limit=1,
+ min_sent=0,
+ offset=0,
+ period="period",
+ sort="sent",
+ status="active",
+ timezone="timezone",
+ )
+ assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list_by_tenant(self, async_client: AsyncArk) -> None:
+ response = await async_client.usage.with_raw_response.list_by_tenant()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list_by_tenant(self, async_client: AsyncArk) -> None:
+ async with async_client.usage.with_streaming_response.list_by_tenant() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_tenant_timeseries_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ granularity="hour",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
+ response = await async_client.usage.with_raw_response.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
+ async with async_client.usage.with_streaming_response.retrieve_tenant_timeseries(
+ tenant_id="cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = await response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.usage.with_raw_response.retrieve_tenant_timeseries(
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_tenant_usage_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
+ response = await async_client.usage.with_raw_response.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
+ async with async_client.usage.with_streaming_response.retrieve_tenant_usage(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.usage.with_raw_response.retrieve_tenant_usage(
+ tenant_id="",
+ )
From b52928d6147770b7c3a68001dad6d3861ff43967 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 4 Feb 2026 14:43:58 +0000
Subject: [PATCH 3/6] feat(api): standardization improvements
---
.stats.yml | 4 ++--
src/ark/types/tenant.py | 6 ++++--
tests/api_resources/test_tenants.py | 8 ++++----
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 136030f..0de1e74 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 50
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-80a061ee30021d968068b710420ca250a2a4cd6e9cec16f2ea5144afed2a6e33.yml
-openapi_spec_hash: 661c8877c1efd5c25f5ebc634e764a72
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-4f8eeb0b9933313def1a9a4184201cb83a96fc2f6de340e7ad52bd216e01adf7.yml
+openapi_spec_hash: af08acc45080ae78054e82214014d34c
config_hash: 569f48c6e4612e54e4db8a875be71e1a
diff --git a/src/ark/types/tenant.py b/src/ark/types/tenant.py
index 5c1591a..1140756 100644
--- a/src/ark/types/tenant.py
+++ b/src/ark/types/tenant.py
@@ -4,6 +4,8 @@
from datetime import datetime
from typing_extensions import Literal
+from pydantic import Field as FieldInfo
+
from .._models import BaseModel
__all__ = ["Tenant"]
@@ -13,7 +15,7 @@ class Tenant(BaseModel):
id: str
"""Unique identifier for the tenant"""
- created_at: datetime
+ created_at: datetime = FieldInfo(alias="createdAt")
"""When the tenant was created"""
metadata: Dict[str, Union[str, float, bool, None]]
@@ -30,5 +32,5 @@ class Tenant(BaseModel):
- `archived` - Soft-deleted
"""
- updated_at: datetime
+ updated_at: datetime = FieldInfo(alias="updatedAt")
"""When the tenant was last updated"""
diff --git a/tests/api_resources/test_tenants.py b/tests/api_resources/test_tenants.py
index 0f02dd2..0d4c436 100644
--- a/tests/api_resources/test_tenants.py
+++ b/tests/api_resources/test_tenants.py
@@ -37,7 +37,7 @@ def test_method_create_with_all_params(self, client: Ark) -> None:
name="Acme Corp",
metadata={
"plan": "pro",
- "internal_id": "cust_12345",
+ "internalId": "cust_12345",
"region": "us-west",
},
)
@@ -118,7 +118,7 @@ def test_method_update_with_all_params(self, client: Ark) -> None:
tenant_id="cm6abc123def456",
metadata={
"plan": "pro",
- "internal_id": "cust_12345",
+ "internalId": "cust_12345",
"region": "us-west",
},
name="Acme Corporation",
@@ -248,7 +248,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> No
name="Acme Corp",
metadata={
"plan": "pro",
- "internal_id": "cust_12345",
+ "internalId": "cust_12345",
"region": "us-west",
},
)
@@ -329,7 +329,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> No
tenant_id="cm6abc123def456",
metadata={
"plan": "pro",
- "internal_id": "cust_12345",
+ "internalId": "cust_12345",
"region": "us-west",
},
name="Acme Corporation",
From 0e54a042195b1134b7c5cba9ee2ca4b98f35b361 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 10:41:09 +0000
Subject: [PATCH 4/6] feat(api): endpoint updates
Update endpoints for v2
---
.stats.yml | 8 +-
README.md | 16 +-
api.md | 222 +++---
src/ark/_client.py | 222 +-----
src/ark/pagination.py | 108 +--
src/ark/resources/__init__.py | 68 +-
src/ark/resources/limits.py | 4 +-
src/ark/resources/tenants/__init__.py | 70 ++
src/ark/resources/tenants/credentials.py | 8 +-
src/ark/resources/{ => tenants}/domains.py | 164 ++--
.../resources/{ => tenants}/suppressions.py | 187 ++---
src/ark/resources/tenants/tenants.py | 160 ++++
src/ark/resources/{ => tenants}/tracking.py | 120 ++-
src/ark/resources/tenants/usage.py | 402 ++++++++++
src/ark/resources/{ => tenants}/webhooks.py | 157 +++-
src/ark/resources/usage.py | 720 ++++++------------
src/ark/types/__init__.py | 55 +-
src/ark/types/bulk_tenant_usage.py | 78 --
src/ark/types/domain_list_params.py | 12 -
src/ark/types/org_usage_summary.py | 47 ++
.../types/suppression_bulk_create_params.py | 19 -
.../types/suppression_bulk_create_response.py | 32 -
src/ark/types/tenant_usage_item.py | 34 +
src/ark/types/tenants/__init__.py | 41 +
src/ark/types/{ => tenants}/dns_record.py | 2 +-
.../{ => tenants}/domain_create_params.py | 3 -
.../{ => tenants}/domain_create_response.py | 4 +-
.../{ => tenants}/domain_delete_response.py | 4 +-
.../{ => tenants}/domain_list_response.py | 4 +-
.../{ => tenants}/domain_retrieve_response.py | 4 +-
.../{ => tenants}/domain_verify_response.py | 4 +-
.../suppression_create_params.py | 0
.../suppression_create_response.py | 4 +-
.../suppression_delete_response.py | 4 +-
.../{ => tenants}/suppression_list_params.py | 2 +-
.../suppression_list_response.py | 2 +-
.../suppression_retrieve_response.py | 4 +-
src/ark/types/{ => tenants}/tenant_usage.py | 8 +-
.../{ => tenants}/tenant_usage_timeseries.py | 4 +-
src/ark/types/{ => tenants}/track_domain.py | 2 +-
.../{ => tenants}/tracking_create_params.py | 2 +-
.../{ => tenants}/tracking_create_response.py | 4 +-
.../{ => tenants}/tracking_delete_response.py | 4 +-
.../{ => tenants}/tracking_list_response.py | 4 +-
.../tracking_retrieve_response.py | 4 +-
.../{ => tenants}/tracking_update_params.py | 6 +-
.../{ => tenants}/tracking_update_response.py | 4 +-
.../{ => tenants}/tracking_verify_response.py | 4 +-
.../usage_retrieve_params.py} | 4 +-
.../usage_retrieve_response.py} | 8 +-
.../usage_retrieve_timeseries_params.py} | 4 +-
.../usage_retrieve_timeseries_response.py} | 8 +-
.../{ => tenants}/webhook_create_params.py | 2 +-
.../{ => tenants}/webhook_create_response.py | 4 +-
.../{ => tenants}/webhook_delete_response.py | 4 +-
.../webhook_list_deliveries_params.py | 6 +-
.../webhook_list_deliveries_response.py | 4 +-
.../{ => tenants}/webhook_list_response.py | 4 +-
.../webhook_replay_delivery_response.py | 4 +-
.../webhook_retrieve_delivery_response.py | 4 +-
.../webhook_retrieve_response.py | 4 +-
.../{ => tenants}/webhook_test_params.py | 6 +-
.../{ => tenants}/webhook_test_response.py | 4 +-
.../{ => tenants}/webhook_update_params.py | 8 +-
.../{ => tenants}/webhook_update_response.py | 4 +-
src/ark/types/usage_export_params.py | 19 +-
src/ark/types/usage_list_by_tenant_params.py | 30 -
src/ark/types/usage_list_tenants_params.py | 51 ++
src/ark/types/usage_retrieve_params.py | 23 +
src/ark/types/usage_retrieve_response.py | 20 -
.../{ => tenants}/test_domains.py | 248 ++++--
.../{ => tenants}/test_suppressions.py | 257 ++++---
.../{ => tenants}/test_tracking.py | 298 ++++++--
tests/api_resources/tenants/test_usage.py | 217 ++++++
.../{ => tenants}/test_webhooks.py | 506 ++++++++----
tests/api_resources/test_usage.py | 312 ++------
76 files changed, 2874 insertions(+), 2230 deletions(-)
rename src/ark/resources/{ => tenants}/domains.py (80%)
rename src/ark/resources/{ => tenants}/suppressions.py (73%)
rename src/ark/resources/{ => tenants}/tracking.py (84%)
create mode 100644 src/ark/resources/tenants/usage.py
rename src/ark/resources/{ => tenants}/webhooks.py (87%)
delete mode 100644 src/ark/types/bulk_tenant_usage.py
delete mode 100644 src/ark/types/domain_list_params.py
create mode 100644 src/ark/types/org_usage_summary.py
delete mode 100644 src/ark/types/suppression_bulk_create_params.py
delete mode 100644 src/ark/types/suppression_bulk_create_response.py
create mode 100644 src/ark/types/tenant_usage_item.py
rename src/ark/types/{ => tenants}/dns_record.py (98%)
rename src/ark/types/{ => tenants}/domain_create_params.py (80%)
rename src/ark/types/{ => tenants}/domain_create_response.py (98%)
rename src/ark/types/{ => tenants}/domain_delete_response.py (81%)
rename src/ark/types/{ => tenants}/domain_list_response.py (92%)
rename src/ark/types/{ => tenants}/domain_retrieve_response.py (98%)
rename src/ark/types/{ => tenants}/domain_verify_response.py (98%)
rename src/ark/types/{ => tenants}/suppression_create_params.py (100%)
rename src/ark/types/{ => tenants}/suppression_create_response.py (89%)
rename src/ark/types/{ => tenants}/suppression_delete_response.py (82%)
rename src/ark/types/{ => tenants}/suppression_list_params.py (90%)
rename src/ark/types/{ => tenants}/suppression_list_response.py (92%)
rename src/ark/types/{ => tenants}/suppression_retrieve_response.py (91%)
rename src/ark/types/{ => tenants}/tenant_usage.py (80%)
rename src/ark/types/{ => tenants}/tenant_usage_timeseries.py (93%)
rename src/ark/types/{ => tenants}/track_domain.py (98%)
rename src/ark/types/{ => tenants}/tracking_create_params.py (96%)
rename src/ark/types/{ => tenants}/tracking_create_response.py (81%)
rename src/ark/types/{ => tenants}/tracking_delete_response.py (81%)
rename src/ark/types/{ => tenants}/tracking_list_response.py (86%)
rename src/ark/types/{ => tenants}/tracking_retrieve_response.py (81%)
rename src/ark/types/{ => tenants}/tracking_update_params.py (83%)
rename src/ark/types/{ => tenants}/tracking_update_response.py (81%)
rename src/ark/types/{ => tenants}/tracking_verify_response.py (94%)
rename src/ark/types/{usage_retrieve_tenant_usage_params.py => tenants/usage_retrieve_params.py} (84%)
rename src/ark/types/{usage_retrieve_tenant_usage_response.py => tenants/usage_retrieve_response.py} (65%)
rename src/ark/types/{usage_retrieve_tenant_timeseries_params.py => tenants/usage_retrieve_timeseries_params.py} (79%)
rename src/ark/types/{usage_retrieve_tenant_timeseries_response.py => tenants/usage_retrieve_timeseries_response.py} (66%)
rename src/ark/types/{ => tenants}/webhook_create_params.py (97%)
rename src/ark/types/{ => tenants}/webhook_create_response.py (93%)
rename src/ark/types/{ => tenants}/webhook_delete_response.py (81%)
rename src/ark/types/{ => tenants}/webhook_list_deliveries_params.py (83%)
rename src/ark/types/{ => tenants}/webhook_list_deliveries_response.py (96%)
rename src/ark/types/{ => tenants}/webhook_list_response.py (87%)
rename src/ark/types/{ => tenants}/webhook_replay_delivery_response.py (93%)
rename src/ark/types/{ => tenants}/webhook_retrieve_delivery_response.py (97%)
rename src/ark/types/{ => tenants}/webhook_retrieve_response.py (93%)
rename src/ark/types/{ => tenants}/webhook_test_params.py (75%)
rename src/ark/types/{ => tenants}/webhook_test_response.py (92%)
rename src/ark/types/{ => tenants}/webhook_update_params.py (67%)
rename src/ark/types/{ => tenants}/webhook_update_response.py (93%)
delete mode 100644 src/ark/types/usage_list_by_tenant_params.py
create mode 100644 src/ark/types/usage_list_tenants_params.py
create mode 100644 src/ark/types/usage_retrieve_params.py
delete mode 100644 src/ark/types/usage_retrieve_response.py
rename tests/api_resources/{ => tenants}/test_domains.py (62%)
rename tests/api_resources/{ => tenants}/test_suppressions.py (62%)
rename tests/api_resources/{ => tenants}/test_tracking.py (61%)
create mode 100644 tests/api_resources/tenants/test_usage.py
rename tests/api_resources/{ => tenants}/test_webhooks.py (60%)
diff --git a/.stats.yml b/.stats.yml
index 0de1e74..8c774e8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 50
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-4f8eeb0b9933313def1a9a4184201cb83a96fc2f6de340e7ad52bd216e01adf7.yml
-openapi_spec_hash: af08acc45080ae78054e82214014d34c
-config_hash: 569f48c6e4612e54e4db8a875be71e1a
+configured_endpoints: 49
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-37bce0c9a4d738ca0eed70eb9c19cd4528a79eec733c2b4366217fb2c31abd78.yml
+openapi_spec_hash: 7dbd1f812cf95dfa8ba631ced146e255
+config_hash: 1c0067ab4449e3fb20b923cd64edc829
diff --git a/README.md b/README.md
index bcf106d..b51d0b6 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ response = client.emails.send(
to=["user@example.com"],
html="
client.emails.send_batch(\*\*params) -> EmailSendBatchResponse
- client.emails.send_raw(\*\*params) -> EmailSendRawResponse
-# Domains
+# Logs
Types:
```python
-from ark.types import (
- DNSRecord,
- DomainCreateResponse,
- DomainRetrieveResponse,
- DomainListResponse,
- DomainDeleteResponse,
- DomainVerifyResponse,
-)
+from ark.types import LogEntry, LogEntryDetail, LogRetrieveResponse
```
Methods:
-- client.domains.create(\*\*params) -> DomainCreateResponse
-- client.domains.retrieve(domain_id) -> DomainRetrieveResponse
-- client.domains.list(\*\*params) -> DomainListResponse
-- client.domains.delete(domain_id) -> DomainDeleteResponse
-- client.domains.verify(domain_id) -> DomainVerifyResponse
+- client.logs.retrieve(request_id) -> LogRetrieveResponse
+- client.logs.list(\*\*params) -> SyncPageNumberPagination[LogEntry]
-# Suppressions
+# Usage
Types:
```python
from ark.types import (
- SuppressionCreateResponse,
- SuppressionRetrieveResponse,
- SuppressionListResponse,
- SuppressionDeleteResponse,
- SuppressionBulkCreateResponse,
+ EmailCounts,
+ EmailRates,
+ OrgUsageSummary,
+ TenantUsageItem,
+ UsagePeriod,
+ UsageExportResponse,
)
```
Methods:
-- client.suppressions.create(\*\*params) -> SuppressionCreateResponse
-- client.suppressions.retrieve(email) -> SuppressionRetrieveResponse
-- client.suppressions.list(\*\*params) -> SyncPageNumberPagination[SuppressionListResponse]
-- client.suppressions.delete(email) -> SuppressionDeleteResponse
-- client.suppressions.bulk_create(\*\*params) -> SuppressionBulkCreateResponse
+- client.usage.retrieve(\*\*params) -> OrgUsageSummary
+- client.usage.export(\*\*params) -> UsageExportResponse
+- client.usage.list_tenants(\*\*params) -> SyncPageNumberPagination[TenantUsageItem]
-# Webhooks
+# Limits
+
+Types:
+
+```python
+from ark.types import LimitsData, LimitRetrieveResponse
+```
+
+Methods:
+
+- client.limits.retrieve() -> LimitRetrieveResponse
+
+# Tenants
Types:
```python
from ark.types import (
- WebhookCreateResponse,
- WebhookRetrieveResponse,
- WebhookUpdateResponse,
- WebhookListResponse,
- WebhookDeleteResponse,
- WebhookListDeliveriesResponse,
- WebhookReplayDeliveryResponse,
- WebhookRetrieveDeliveryResponse,
- WebhookTestResponse,
+ Tenant,
+ TenantCreateResponse,
+ TenantRetrieveResponse,
+ TenantUpdateResponse,
+ TenantDeleteResponse,
)
```
Methods:
-- client.webhooks.create(\*\*params) -> WebhookCreateResponse
-- client.webhooks.retrieve(webhook_id) -> WebhookRetrieveResponse
-- client.webhooks.update(webhook_id, \*\*params) -> WebhookUpdateResponse
-- client.webhooks.list() -> WebhookListResponse
-- client.webhooks.delete(webhook_id) -> WebhookDeleteResponse
-- client.webhooks.list_deliveries(webhook_id, \*\*params) -> WebhookListDeliveriesResponse
-- client.webhooks.replay_delivery(delivery_id, \*, webhook_id) -> WebhookReplayDeliveryResponse
-- client.webhooks.retrieve_delivery(delivery_id, \*, webhook_id) -> WebhookRetrieveDeliveryResponse
-- client.webhooks.test(webhook_id, \*\*params) -> WebhookTestResponse
+- client.tenants.create(\*\*params) -> TenantCreateResponse
+- client.tenants.retrieve(tenant_id) -> TenantRetrieveResponse
+- client.tenants.update(tenant_id, \*\*params) -> TenantUpdateResponse
+- client.tenants.list(\*\*params) -> SyncPageNumberPagination[Tenant]
+- client.tenants.delete(tenant_id) -> TenantDeleteResponse
-# Tracking
+## Credentials
Types:
```python
-from ark.types import (
- TrackDomain,
- TrackingCreateResponse,
- TrackingRetrieveResponse,
- TrackingUpdateResponse,
- TrackingListResponse,
- TrackingDeleteResponse,
- TrackingVerifyResponse,
+from ark.types.tenants import (
+ CredentialCreateResponse,
+ CredentialRetrieveResponse,
+ CredentialUpdateResponse,
+ CredentialListResponse,
+ CredentialDeleteResponse,
)
```
Methods:
-- client.tracking.create(\*\*params) -> TrackingCreateResponse
-- client.tracking.retrieve(tracking_id) -> TrackingRetrieveResponse
-- client.tracking.update(tracking_id, \*\*params) -> TrackingUpdateResponse
-- client.tracking.list() -> TrackingListResponse
-- client.tracking.delete(tracking_id) -> TrackingDeleteResponse
-- client.tracking.verify(tracking_id) -> TrackingVerifyResponse
+- client.tenants.credentials.create(tenant_id, \*\*params) -> CredentialCreateResponse
+- client.tenants.credentials.retrieve(credential_id, \*, tenant_id, \*\*params) -> CredentialRetrieveResponse
+- client.tenants.credentials.update(credential_id, \*, tenant_id, \*\*params) -> CredentialUpdateResponse
+- client.tenants.credentials.list(tenant_id, \*\*params) -> SyncPageNumberPagination[CredentialListResponse]
+- client.tenants.credentials.delete(credential_id, \*, tenant_id) -> CredentialDeleteResponse
-# Logs
+## Domains
Types:
```python
-from ark.types import LogEntry, LogEntryDetail, LogRetrieveResponse
+from ark.types.tenants import (
+ DNSRecord,
+ DomainCreateResponse,
+ DomainRetrieveResponse,
+ DomainListResponse,
+ DomainDeleteResponse,
+ DomainVerifyResponse,
+)
```
Methods:
-- client.logs.retrieve(request_id) -> LogRetrieveResponse
-- client.logs.list(\*\*params) -> SyncPageNumberPagination[LogEntry]
+- client.tenants.domains.create(tenant_id, \*\*params) -> DomainCreateResponse
+- client.tenants.domains.retrieve(domain_id, \*, tenant_id) -> DomainRetrieveResponse
+- client.tenants.domains.list(tenant_id) -> DomainListResponse
+- client.tenants.domains.delete(domain_id, \*, tenant_id) -> DomainDeleteResponse
+- client.tenants.domains.verify(domain_id, \*, tenant_id) -> DomainVerifyResponse
-# Limits
+## Suppressions
Types:
```python
-from ark.types import LimitsData, LimitRetrieveResponse
+from ark.types.tenants import (
+ SuppressionCreateResponse,
+ SuppressionRetrieveResponse,
+ SuppressionListResponse,
+ SuppressionDeleteResponse,
+)
```
Methods:
-- client.limits.retrieve() -> LimitRetrieveResponse
+- client.tenants.suppressions.create(tenant_id, \*\*params) -> SuppressionCreateResponse
+- client.tenants.suppressions.retrieve(email, \*, tenant_id) -> SuppressionRetrieveResponse
+- client.tenants.suppressions.list(tenant_id, \*\*params) -> SyncPageNumberPagination[SuppressionListResponse]
+- client.tenants.suppressions.delete(email, \*, tenant_id) -> SuppressionDeleteResponse
-# Usage
+## Webhooks
Types:
```python
-from ark.types import (
- BulkTenantUsage,
- EmailCounts,
- EmailRates,
- TenantUsage,
- TenantUsageTimeseries,
- UsagePeriod,
- UsageRetrieveResponse,
- UsageExportResponse,
- UsageRetrieveTenantTimeseriesResponse,
- UsageRetrieveTenantUsageResponse,
+from ark.types.tenants import (
+ WebhookCreateResponse,
+ WebhookRetrieveResponse,
+ WebhookUpdateResponse,
+ WebhookListResponse,
+ WebhookDeleteResponse,
+ WebhookListDeliveriesResponse,
+ WebhookReplayDeliveryResponse,
+ WebhookRetrieveDeliveryResponse,
+ WebhookTestResponse,
)
```
Methods:
-- client.usage.retrieve() -> UsageRetrieveResponse
-- client.usage.export(\*\*params) -> UsageExportResponse
-- client.usage.list_by_tenant(\*\*params) -> SyncOffsetPagination[Tenant]
-- client.usage.retrieve_tenant_timeseries(tenant_id, \*\*params) -> UsageRetrieveTenantTimeseriesResponse
-- client.usage.retrieve_tenant_usage(tenant_id, \*\*params) -> UsageRetrieveTenantUsageResponse
+- client.tenants.webhooks.create(tenant_id, \*\*params) -> WebhookCreateResponse
+- client.tenants.webhooks.retrieve(webhook_id, \*, tenant_id) -> WebhookRetrieveResponse
+- client.tenants.webhooks.update(webhook_id, \*, tenant_id, \*\*params) -> WebhookUpdateResponse
+- client.tenants.webhooks.list(tenant_id) -> WebhookListResponse
+- client.tenants.webhooks.delete(webhook_id, \*, tenant_id) -> WebhookDeleteResponse
+- client.tenants.webhooks.list_deliveries(webhook_id, \*, tenant_id, \*\*params) -> WebhookListDeliveriesResponse
+- client.tenants.webhooks.replay_delivery(delivery_id, \*, tenant_id, webhook_id) -> WebhookReplayDeliveryResponse
+- client.tenants.webhooks.retrieve_delivery(delivery_id, \*, tenant_id, webhook_id) -> WebhookRetrieveDeliveryResponse
+- client.tenants.webhooks.test(webhook_id, \*, tenant_id, \*\*params) -> WebhookTestResponse
-# Tenants
+## Tracking
Types:
```python
-from ark.types import (
- Tenant,
- TenantCreateResponse,
- TenantRetrieveResponse,
- TenantUpdateResponse,
- TenantDeleteResponse,
+from ark.types.tenants import (
+ TrackDomain,
+ TrackingCreateResponse,
+ TrackingRetrieveResponse,
+ TrackingUpdateResponse,
+ TrackingListResponse,
+ TrackingDeleteResponse,
+ TrackingVerifyResponse,
)
```
Methods:
-- client.tenants.create(\*\*params) -> TenantCreateResponse
-- client.tenants.retrieve(tenant_id) -> TenantRetrieveResponse
-- client.tenants.update(tenant_id, \*\*params) -> TenantUpdateResponse
-- client.tenants.list(\*\*params) -> SyncPageNumberPagination[Tenant]
-- client.tenants.delete(tenant_id) -> TenantDeleteResponse
+- client.tenants.tracking.create(tenant_id, \*\*params) -> TrackingCreateResponse
+- client.tenants.tracking.retrieve(tracking_id, \*, tenant_id) -> TrackingRetrieveResponse
+- client.tenants.tracking.update(tracking_id, \*, tenant_id, \*\*params) -> TrackingUpdateResponse
+- client.tenants.tracking.list(tenant_id) -> TrackingListResponse
+- client.tenants.tracking.delete(tracking_id, \*, tenant_id) -> TrackingDeleteResponse
+- client.tenants.tracking.verify(tracking_id, \*, tenant_id) -> TrackingVerifyResponse
-## Credentials
+## Usage
Types:
```python
from ark.types.tenants import (
- CredentialCreateResponse,
- CredentialRetrieveResponse,
- CredentialUpdateResponse,
- CredentialListResponse,
- CredentialDeleteResponse,
+ TenantUsage,
+ TenantUsageTimeseries,
+ UsageRetrieveResponse,
+ UsageRetrieveTimeseriesResponse,
)
```
Methods:
-- client.tenants.credentials.create(tenant_id, \*\*params) -> CredentialCreateResponse
-- client.tenants.credentials.retrieve(credential_id, \*, tenant_id, \*\*params) -> CredentialRetrieveResponse
-- client.tenants.credentials.update(credential_id, \*, tenant_id, \*\*params) -> CredentialUpdateResponse
-- client.tenants.credentials.list(tenant_id, \*\*params) -> SyncPageNumberPagination[CredentialListResponse]
-- client.tenants.credentials.delete(credential_id, \*, tenant_id) -> CredentialDeleteResponse
+- client.tenants.usage.retrieve(tenant_id, \*\*params) -> UsageRetrieveResponse
+- client.tenants.usage.retrieve_timeseries(tenant_id, \*\*params) -> UsageRetrieveTimeseriesResponse
diff --git a/src/ark/_client.py b/src/ark/_client.py
index 8ce241f..d9d5633 100644
--- a/src/ark/_client.py
+++ b/src/ark/_client.py
@@ -31,15 +31,11 @@
)
if TYPE_CHECKING:
- from .resources import logs, usage, emails, limits, domains, tenants, tracking, webhooks, suppressions
+ from .resources import logs, usage, emails, limits, tenants
from .resources.logs import LogsResource, AsyncLogsResource
from .resources.usage import UsageResource, AsyncUsageResource
from .resources.emails import EmailsResource, AsyncEmailsResource
from .resources.limits import LimitsResource, AsyncLimitsResource
- from .resources.domains import DomainsResource, AsyncDomainsResource
- from .resources.tracking import TrackingResource, AsyncTrackingResource
- from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
- from .resources.suppressions import SuppressionsResource, AsyncSuppressionsResource
from .resources.tenants.tenants import TenantsResource, AsyncTenantsResource
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Ark", "AsyncArk", "Client", "AsyncClient"]
@@ -106,48 +102,24 @@ def emails(self) -> EmailsResource:
return EmailsResource(self)
- @cached_property
- def domains(self) -> DomainsResource:
- from .resources.domains import DomainsResource
-
- return DomainsResource(self)
-
- @cached_property
- def suppressions(self) -> SuppressionsResource:
- from .resources.suppressions import SuppressionsResource
-
- return SuppressionsResource(self)
-
- @cached_property
- def webhooks(self) -> WebhooksResource:
- from .resources.webhooks import WebhooksResource
-
- return WebhooksResource(self)
-
- @cached_property
- def tracking(self) -> TrackingResource:
- from .resources.tracking import TrackingResource
-
- return TrackingResource(self)
-
@cached_property
def logs(self) -> LogsResource:
from .resources.logs import LogsResource
return LogsResource(self)
- @cached_property
- def limits(self) -> LimitsResource:
- from .resources.limits import LimitsResource
-
- return LimitsResource(self)
-
@cached_property
def usage(self) -> UsageResource:
from .resources.usage import UsageResource
return UsageResource(self)
+ @cached_property
+ def limits(self) -> LimitsResource:
+ from .resources.limits import LimitsResource
+
+ return LimitsResource(self)
+
@cached_property
def tenants(self) -> TenantsResource:
from .resources.tenants import TenantsResource
@@ -328,48 +300,24 @@ def emails(self) -> AsyncEmailsResource:
return AsyncEmailsResource(self)
- @cached_property
- def domains(self) -> AsyncDomainsResource:
- from .resources.domains import AsyncDomainsResource
-
- return AsyncDomainsResource(self)
-
- @cached_property
- def suppressions(self) -> AsyncSuppressionsResource:
- from .resources.suppressions import AsyncSuppressionsResource
-
- return AsyncSuppressionsResource(self)
-
- @cached_property
- def webhooks(self) -> AsyncWebhooksResource:
- from .resources.webhooks import AsyncWebhooksResource
-
- return AsyncWebhooksResource(self)
-
- @cached_property
- def tracking(self) -> AsyncTrackingResource:
- from .resources.tracking import AsyncTrackingResource
-
- return AsyncTrackingResource(self)
-
@cached_property
def logs(self) -> AsyncLogsResource:
from .resources.logs import AsyncLogsResource
return AsyncLogsResource(self)
- @cached_property
- def limits(self) -> AsyncLimitsResource:
- from .resources.limits import AsyncLimitsResource
-
- return AsyncLimitsResource(self)
-
@cached_property
def usage(self) -> AsyncUsageResource:
from .resources.usage import AsyncUsageResource
return AsyncUsageResource(self)
+ @cached_property
+ def limits(self) -> AsyncLimitsResource:
+ from .resources.limits import AsyncLimitsResource
+
+ return AsyncLimitsResource(self)
+
@cached_property
def tenants(self) -> AsyncTenantsResource:
from .resources.tenants import AsyncTenantsResource
@@ -501,48 +449,24 @@ def emails(self) -> emails.EmailsResourceWithRawResponse:
return EmailsResourceWithRawResponse(self._client.emails)
- @cached_property
- def domains(self) -> domains.DomainsResourceWithRawResponse:
- from .resources.domains import DomainsResourceWithRawResponse
-
- return DomainsResourceWithRawResponse(self._client.domains)
-
- @cached_property
- def suppressions(self) -> suppressions.SuppressionsResourceWithRawResponse:
- from .resources.suppressions import SuppressionsResourceWithRawResponse
-
- return SuppressionsResourceWithRawResponse(self._client.suppressions)
-
- @cached_property
- def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse:
- from .resources.webhooks import WebhooksResourceWithRawResponse
-
- return WebhooksResourceWithRawResponse(self._client.webhooks)
-
- @cached_property
- def tracking(self) -> tracking.TrackingResourceWithRawResponse:
- from .resources.tracking import TrackingResourceWithRawResponse
-
- return TrackingResourceWithRawResponse(self._client.tracking)
-
@cached_property
def logs(self) -> logs.LogsResourceWithRawResponse:
from .resources.logs import LogsResourceWithRawResponse
return LogsResourceWithRawResponse(self._client.logs)
- @cached_property
- def limits(self) -> limits.LimitsResourceWithRawResponse:
- from .resources.limits import LimitsResourceWithRawResponse
-
- return LimitsResourceWithRawResponse(self._client.limits)
-
@cached_property
def usage(self) -> usage.UsageResourceWithRawResponse:
from .resources.usage import UsageResourceWithRawResponse
return UsageResourceWithRawResponse(self._client.usage)
+ @cached_property
+ def limits(self) -> limits.LimitsResourceWithRawResponse:
+ from .resources.limits import LimitsResourceWithRawResponse
+
+ return LimitsResourceWithRawResponse(self._client.limits)
+
@cached_property
def tenants(self) -> tenants.TenantsResourceWithRawResponse:
from .resources.tenants import TenantsResourceWithRawResponse
@@ -562,48 +486,24 @@ def emails(self) -> emails.AsyncEmailsResourceWithRawResponse:
return AsyncEmailsResourceWithRawResponse(self._client.emails)
- @cached_property
- def domains(self) -> domains.AsyncDomainsResourceWithRawResponse:
- from .resources.domains import AsyncDomainsResourceWithRawResponse
-
- return AsyncDomainsResourceWithRawResponse(self._client.domains)
-
- @cached_property
- def suppressions(self) -> suppressions.AsyncSuppressionsResourceWithRawResponse:
- from .resources.suppressions import AsyncSuppressionsResourceWithRawResponse
-
- return AsyncSuppressionsResourceWithRawResponse(self._client.suppressions)
-
- @cached_property
- def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse:
- from .resources.webhooks import AsyncWebhooksResourceWithRawResponse
-
- return AsyncWebhooksResourceWithRawResponse(self._client.webhooks)
-
- @cached_property
- def tracking(self) -> tracking.AsyncTrackingResourceWithRawResponse:
- from .resources.tracking import AsyncTrackingResourceWithRawResponse
-
- return AsyncTrackingResourceWithRawResponse(self._client.tracking)
-
@cached_property
def logs(self) -> logs.AsyncLogsResourceWithRawResponse:
from .resources.logs import AsyncLogsResourceWithRawResponse
return AsyncLogsResourceWithRawResponse(self._client.logs)
- @cached_property
- def limits(self) -> limits.AsyncLimitsResourceWithRawResponse:
- from .resources.limits import AsyncLimitsResourceWithRawResponse
-
- return AsyncLimitsResourceWithRawResponse(self._client.limits)
-
@cached_property
def usage(self) -> usage.AsyncUsageResourceWithRawResponse:
from .resources.usage import AsyncUsageResourceWithRawResponse
return AsyncUsageResourceWithRawResponse(self._client.usage)
+ @cached_property
+ def limits(self) -> limits.AsyncLimitsResourceWithRawResponse:
+ from .resources.limits import AsyncLimitsResourceWithRawResponse
+
+ return AsyncLimitsResourceWithRawResponse(self._client.limits)
+
@cached_property
def tenants(self) -> tenants.AsyncTenantsResourceWithRawResponse:
from .resources.tenants import AsyncTenantsResourceWithRawResponse
@@ -623,48 +523,24 @@ def emails(self) -> emails.EmailsResourceWithStreamingResponse:
return EmailsResourceWithStreamingResponse(self._client.emails)
- @cached_property
- def domains(self) -> domains.DomainsResourceWithStreamingResponse:
- from .resources.domains import DomainsResourceWithStreamingResponse
-
- return DomainsResourceWithStreamingResponse(self._client.domains)
-
- @cached_property
- def suppressions(self) -> suppressions.SuppressionsResourceWithStreamingResponse:
- from .resources.suppressions import SuppressionsResourceWithStreamingResponse
-
- return SuppressionsResourceWithStreamingResponse(self._client.suppressions)
-
- @cached_property
- def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse:
- from .resources.webhooks import WebhooksResourceWithStreamingResponse
-
- return WebhooksResourceWithStreamingResponse(self._client.webhooks)
-
- @cached_property
- def tracking(self) -> tracking.TrackingResourceWithStreamingResponse:
- from .resources.tracking import TrackingResourceWithStreamingResponse
-
- return TrackingResourceWithStreamingResponse(self._client.tracking)
-
@cached_property
def logs(self) -> logs.LogsResourceWithStreamingResponse:
from .resources.logs import LogsResourceWithStreamingResponse
return LogsResourceWithStreamingResponse(self._client.logs)
- @cached_property
- def limits(self) -> limits.LimitsResourceWithStreamingResponse:
- from .resources.limits import LimitsResourceWithStreamingResponse
-
- return LimitsResourceWithStreamingResponse(self._client.limits)
-
@cached_property
def usage(self) -> usage.UsageResourceWithStreamingResponse:
from .resources.usage import UsageResourceWithStreamingResponse
return UsageResourceWithStreamingResponse(self._client.usage)
+ @cached_property
+ def limits(self) -> limits.LimitsResourceWithStreamingResponse:
+ from .resources.limits import LimitsResourceWithStreamingResponse
+
+ return LimitsResourceWithStreamingResponse(self._client.limits)
+
@cached_property
def tenants(self) -> tenants.TenantsResourceWithStreamingResponse:
from .resources.tenants import TenantsResourceWithStreamingResponse
@@ -684,48 +560,24 @@ def emails(self) -> emails.AsyncEmailsResourceWithStreamingResponse:
return AsyncEmailsResourceWithStreamingResponse(self._client.emails)
- @cached_property
- def domains(self) -> domains.AsyncDomainsResourceWithStreamingResponse:
- from .resources.domains import AsyncDomainsResourceWithStreamingResponse
-
- return AsyncDomainsResourceWithStreamingResponse(self._client.domains)
-
- @cached_property
- def suppressions(self) -> suppressions.AsyncSuppressionsResourceWithStreamingResponse:
- from .resources.suppressions import AsyncSuppressionsResourceWithStreamingResponse
-
- return AsyncSuppressionsResourceWithStreamingResponse(self._client.suppressions)
-
- @cached_property
- def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse:
- from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse
-
- return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks)
-
- @cached_property
- def tracking(self) -> tracking.AsyncTrackingResourceWithStreamingResponse:
- from .resources.tracking import AsyncTrackingResourceWithStreamingResponse
-
- return AsyncTrackingResourceWithStreamingResponse(self._client.tracking)
-
@cached_property
def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse:
from .resources.logs import AsyncLogsResourceWithStreamingResponse
return AsyncLogsResourceWithStreamingResponse(self._client.logs)
- @cached_property
- def limits(self) -> limits.AsyncLimitsResourceWithStreamingResponse:
- from .resources.limits import AsyncLimitsResourceWithStreamingResponse
-
- return AsyncLimitsResourceWithStreamingResponse(self._client.limits)
-
@cached_property
def usage(self) -> usage.AsyncUsageResourceWithStreamingResponse:
from .resources.usage import AsyncUsageResourceWithStreamingResponse
return AsyncUsageResourceWithStreamingResponse(self._client.usage)
+ @cached_property
+ def limits(self) -> limits.AsyncLimitsResourceWithStreamingResponse:
+ from .resources.limits import AsyncLimitsResourceWithStreamingResponse
+
+ return AsyncLimitsResourceWithStreamingResponse(self._client.limits)
+
@cached_property
def tenants(self) -> tenants.AsyncTenantsResourceWithStreamingResponse:
from .resources.tenants import AsyncTenantsResourceWithStreamingResponse
diff --git a/src/ark/pagination.py b/src/ark/pagination.py
index 6e0c4b0..234b226 100644
--- a/src/ark/pagination.py
+++ b/src/ark/pagination.py
@@ -5,18 +5,10 @@
from pydantic import Field as FieldInfo
-from ._models import BaseModel, GenericModel
+from ._models import BaseModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = [
- "PageNumberPaginationMeta",
- "SyncPageNumberPagination",
- "AsyncPageNumberPagination",
- "OffsetPaginationData",
- "OffsetPaginationPagination",
- "SyncOffsetPagination",
- "AsyncOffsetPagination",
-]
+__all__ = ["PageNumberPaginationMeta", "SyncPageNumberPagination", "AsyncPageNumberPagination"]
_T = TypeVar("_T")
@@ -79,99 +71,3 @@ def next_page_info(self) -> Optional[PageInfo]:
return None
return PageInfo(params={"page": current_page + 1})
-
-
-class OffsetPaginationPagination(BaseModel):
- has_more: Optional[bool] = None
-
- limit: Optional[int] = None
-
- offset: Optional[int] = None
-
- total: Optional[int] = None
-
-
-class OffsetPaginationData(GenericModel, Generic[_T]):
- pagination: Optional[OffsetPaginationPagination] = None
-
- tenants: Optional[List[_T]] = None
-
-
-class SyncOffsetPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
- data: Optional[OffsetPaginationData[_T]] = None
-
- @override
- def _get_page_items(self) -> List[_T]:
- tenants = None
- if self.data is not None:
- if self.data.tenants is not None:
- tenants = self.data.tenants
- if not tenants:
- return []
- return tenants
-
- @override
- def next_page_info(self) -> Optional[PageInfo]:
- offset = None
- if self.data is not None:
- if self.data.pagination is not None:
- if self.data.pagination.offset is not None:
- offset = self.data.pagination.offset
- if offset is None:
- return None # type: ignore[unreachable]
-
- length = len(self._get_page_items())
- current_count = offset + length
-
- total = None
- if self.data is not None:
- if self.data.pagination is not None:
- if self.data.pagination.total is not None:
- total = self.data.pagination.total
- if total is None:
- return None
-
- if current_count < total:
- return PageInfo(params={"offset": current_count})
-
- return None
-
-
-class AsyncOffsetPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
- data: Optional[OffsetPaginationData[_T]] = None
-
- @override
- def _get_page_items(self) -> List[_T]:
- tenants = None
- if self.data is not None:
- if self.data.tenants is not None:
- tenants = self.data.tenants
- if not tenants:
- return []
- return tenants
-
- @override
- def next_page_info(self) -> Optional[PageInfo]:
- offset = None
- if self.data is not None:
- if self.data.pagination is not None:
- if self.data.pagination.offset is not None:
- offset = self.data.pagination.offset
- if offset is None:
- return None # type: ignore[unreachable]
-
- length = len(self._get_page_items())
- current_count = offset + length
-
- total = None
- if self.data is not None:
- if self.data.pagination is not None:
- if self.data.pagination.total is not None:
- total = self.data.pagination.total
- if total is None:
- return None
-
- if current_count < total:
- return PageInfo(params={"offset": current_count})
-
- return None
diff --git a/src/ark/resources/__init__.py b/src/ark/resources/__init__.py
index e14e930..a98e05b 100644
--- a/src/ark/resources/__init__.py
+++ b/src/ark/resources/__init__.py
@@ -32,14 +32,6 @@
LimitsResourceWithStreamingResponse,
AsyncLimitsResourceWithStreamingResponse,
)
-from .domains import (
- DomainsResource,
- AsyncDomainsResource,
- DomainsResourceWithRawResponse,
- AsyncDomainsResourceWithRawResponse,
- DomainsResourceWithStreamingResponse,
- AsyncDomainsResourceWithStreamingResponse,
-)
from .tenants import (
TenantsResource,
AsyncTenantsResource,
@@ -48,30 +40,6 @@
TenantsResourceWithStreamingResponse,
AsyncTenantsResourceWithStreamingResponse,
)
-from .tracking import (
- TrackingResource,
- AsyncTrackingResource,
- TrackingResourceWithRawResponse,
- AsyncTrackingResourceWithRawResponse,
- TrackingResourceWithStreamingResponse,
- AsyncTrackingResourceWithStreamingResponse,
-)
-from .webhooks import (
- WebhooksResource,
- AsyncWebhooksResource,
- WebhooksResourceWithRawResponse,
- AsyncWebhooksResourceWithRawResponse,
- WebhooksResourceWithStreamingResponse,
- AsyncWebhooksResourceWithStreamingResponse,
-)
-from .suppressions import (
- SuppressionsResource,
- AsyncSuppressionsResource,
- SuppressionsResourceWithRawResponse,
- AsyncSuppressionsResourceWithRawResponse,
- SuppressionsResourceWithStreamingResponse,
- AsyncSuppressionsResourceWithStreamingResponse,
-)
__all__ = [
"EmailsResource",
@@ -80,48 +48,24 @@
"AsyncEmailsResourceWithRawResponse",
"EmailsResourceWithStreamingResponse",
"AsyncEmailsResourceWithStreamingResponse",
- "DomainsResource",
- "AsyncDomainsResource",
- "DomainsResourceWithRawResponse",
- "AsyncDomainsResourceWithRawResponse",
- "DomainsResourceWithStreamingResponse",
- "AsyncDomainsResourceWithStreamingResponse",
- "SuppressionsResource",
- "AsyncSuppressionsResource",
- "SuppressionsResourceWithRawResponse",
- "AsyncSuppressionsResourceWithRawResponse",
- "SuppressionsResourceWithStreamingResponse",
- "AsyncSuppressionsResourceWithStreamingResponse",
- "WebhooksResource",
- "AsyncWebhooksResource",
- "WebhooksResourceWithRawResponse",
- "AsyncWebhooksResourceWithRawResponse",
- "WebhooksResourceWithStreamingResponse",
- "AsyncWebhooksResourceWithStreamingResponse",
- "TrackingResource",
- "AsyncTrackingResource",
- "TrackingResourceWithRawResponse",
- "AsyncTrackingResourceWithRawResponse",
- "TrackingResourceWithStreamingResponse",
- "AsyncTrackingResourceWithStreamingResponse",
"LogsResource",
"AsyncLogsResource",
"LogsResourceWithRawResponse",
"AsyncLogsResourceWithRawResponse",
"LogsResourceWithStreamingResponse",
"AsyncLogsResourceWithStreamingResponse",
- "LimitsResource",
- "AsyncLimitsResource",
- "LimitsResourceWithRawResponse",
- "AsyncLimitsResourceWithRawResponse",
- "LimitsResourceWithStreamingResponse",
- "AsyncLimitsResourceWithStreamingResponse",
"UsageResource",
"AsyncUsageResource",
"UsageResourceWithRawResponse",
"AsyncUsageResourceWithRawResponse",
"UsageResourceWithStreamingResponse",
"AsyncUsageResourceWithStreamingResponse",
+ "LimitsResource",
+ "AsyncLimitsResource",
+ "LimitsResourceWithRawResponse",
+ "AsyncLimitsResourceWithRawResponse",
+ "LimitsResourceWithStreamingResponse",
+ "AsyncLimitsResourceWithStreamingResponse",
"TenantsResource",
"AsyncTenantsResource",
"TenantsResourceWithRawResponse",
diff --git a/src/ark/resources/limits.py b/src/ark/resources/limits.py
index f18db42..8dd9db6 100644
--- a/src/ark/resources/limits.py
+++ b/src/ark/resources/limits.py
@@ -64,7 +64,7 @@ def retrieve(
**Notes:**
- This request counts against your rate limit
- - `sendLimit` may be null if Postal is temporarily unavailable
+ - `sendLimit` may be null if the service is temporarily unavailable
- `billing` is null if billing is not configured
- Send limit resets at the top of each hour
"""
@@ -122,7 +122,7 @@ async def retrieve(
**Notes:**
- This request counts against your rate limit
- - `sendLimit` may be null if Postal is temporarily unavailable
+ - `sendLimit` may be null if the service is temporarily unavailable
- `billing` is null if billing is not configured
- Send limit resets at the top of each hour
"""
diff --git a/src/ark/resources/tenants/__init__.py b/src/ark/resources/tenants/__init__.py
index f05ec88..31a4e7e 100644
--- a/src/ark/resources/tenants/__init__.py
+++ b/src/ark/resources/tenants/__init__.py
@@ -1,5 +1,21 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from .usage import (
+ UsageResource,
+ AsyncUsageResource,
+ UsageResourceWithRawResponse,
+ AsyncUsageResourceWithRawResponse,
+ UsageResourceWithStreamingResponse,
+ AsyncUsageResourceWithStreamingResponse,
+)
+from .domains import (
+ DomainsResource,
+ AsyncDomainsResource,
+ DomainsResourceWithRawResponse,
+ AsyncDomainsResourceWithRawResponse,
+ DomainsResourceWithStreamingResponse,
+ AsyncDomainsResourceWithStreamingResponse,
+)
from .tenants import (
TenantsResource,
AsyncTenantsResource,
@@ -8,6 +24,22 @@
TenantsResourceWithStreamingResponse,
AsyncTenantsResourceWithStreamingResponse,
)
+from .tracking import (
+ TrackingResource,
+ AsyncTrackingResource,
+ TrackingResourceWithRawResponse,
+ AsyncTrackingResourceWithRawResponse,
+ TrackingResourceWithStreamingResponse,
+ AsyncTrackingResourceWithStreamingResponse,
+)
+from .webhooks import (
+ WebhooksResource,
+ AsyncWebhooksResource,
+ WebhooksResourceWithRawResponse,
+ AsyncWebhooksResourceWithRawResponse,
+ WebhooksResourceWithStreamingResponse,
+ AsyncWebhooksResourceWithStreamingResponse,
+)
from .credentials import (
CredentialsResource,
AsyncCredentialsResource,
@@ -16,6 +48,14 @@
CredentialsResourceWithStreamingResponse,
AsyncCredentialsResourceWithStreamingResponse,
)
+from .suppressions import (
+ SuppressionsResource,
+ AsyncSuppressionsResource,
+ SuppressionsResourceWithRawResponse,
+ AsyncSuppressionsResourceWithRawResponse,
+ SuppressionsResourceWithStreamingResponse,
+ AsyncSuppressionsResourceWithStreamingResponse,
+)
__all__ = [
"CredentialsResource",
@@ -24,6 +64,36 @@
"AsyncCredentialsResourceWithRawResponse",
"CredentialsResourceWithStreamingResponse",
"AsyncCredentialsResourceWithStreamingResponse",
+ "DomainsResource",
+ "AsyncDomainsResource",
+ "DomainsResourceWithRawResponse",
+ "AsyncDomainsResourceWithRawResponse",
+ "DomainsResourceWithStreamingResponse",
+ "AsyncDomainsResourceWithStreamingResponse",
+ "SuppressionsResource",
+ "AsyncSuppressionsResource",
+ "SuppressionsResourceWithRawResponse",
+ "AsyncSuppressionsResourceWithRawResponse",
+ "SuppressionsResourceWithStreamingResponse",
+ "AsyncSuppressionsResourceWithStreamingResponse",
+ "WebhooksResource",
+ "AsyncWebhooksResource",
+ "WebhooksResourceWithRawResponse",
+ "AsyncWebhooksResourceWithRawResponse",
+ "WebhooksResourceWithStreamingResponse",
+ "AsyncWebhooksResourceWithStreamingResponse",
+ "TrackingResource",
+ "AsyncTrackingResource",
+ "TrackingResourceWithRawResponse",
+ "AsyncTrackingResourceWithRawResponse",
+ "TrackingResourceWithStreamingResponse",
+ "AsyncTrackingResourceWithStreamingResponse",
+ "UsageResource",
+ "AsyncUsageResource",
+ "UsageResourceWithRawResponse",
+ "AsyncUsageResourceWithRawResponse",
+ "UsageResourceWithStreamingResponse",
+ "AsyncUsageResourceWithStreamingResponse",
"TenantsResource",
"AsyncTenantsResource",
"TenantsResourceWithRawResponse",
diff --git a/src/ark/resources/tenants/credentials.py b/src/ark/resources/tenants/credentials.py
index 98aed9b..2affa6a 100644
--- a/src/ark/resources/tenants/credentials.py
+++ b/src/ark/resources/tenants/credentials.py
@@ -69,7 +69,7 @@ def create(
"""Create a new SMTP or API credential for a tenant.
The credential can be used to
- send emails through Postal on behalf of the tenant.
+ send emails via Ark on behalf of the tenant.
**Important:** The credential key is only returned once at creation time. Store
it securely - you cannot retrieve it again.
@@ -231,7 +231,7 @@ def list(
"""List all SMTP and API credentials for a tenant.
Credentials are used to send
- emails through Postal on behalf of the tenant.
+ emails via Ark on behalf of the tenant.
**Security:** Credential keys are not returned in the list response. Use the
retrieve endpoint with `reveal=true` to get the key.
@@ -349,7 +349,7 @@ async def create(
"""Create a new SMTP or API credential for a tenant.
The credential can be used to
- send emails through Postal on behalf of the tenant.
+ send emails via Ark on behalf of the tenant.
**Important:** The credential key is only returned once at creation time. Store
it securely - you cannot retrieve it again.
@@ -513,7 +513,7 @@ def list(
"""List all SMTP and API credentials for a tenant.
Credentials are used to send
- emails through Postal on behalf of the tenant.
+ emails via Ark on behalf of the tenant.
**Security:** Credential keys are not returned in the list response. Use the
retrieve endpoint with `reveal=true` to get the key.
diff --git a/src/ark/resources/domains.py b/src/ark/resources/tenants/domains.py
similarity index 80%
rename from src/ark/resources/domains.py
rename to src/ark/resources/tenants/domains.py
index 89c1076..e560458 100644
--- a/src/ark/resources/domains.py
+++ b/src/ark/resources/tenants/domains.py
@@ -4,23 +4,23 @@
import httpx
-from ..types import domain_list_params, domain_create_params
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ..._types import Body, Query, Headers, NotGiven, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
-from ..types.domain_list_response import DomainListResponse
-from ..types.domain_create_response import DomainCreateResponse
-from ..types.domain_delete_response import DomainDeleteResponse
-from ..types.domain_verify_response import DomainVerifyResponse
-from ..types.domain_retrieve_response import DomainRetrieveResponse
+from ..._base_client import make_request_options
+from ...types.tenants import domain_create_params
+from ...types.tenants.domain_list_response import DomainListResponse
+from ...types.tenants.domain_create_response import DomainCreateResponse
+from ...types.tenants.domain_delete_response import DomainDeleteResponse
+from ...types.tenants.domain_verify_response import DomainVerifyResponse
+from ...types.tenants.domain_retrieve_response import DomainRetrieveResponse
__all__ = ["DomainsResource", "AsyncDomainsResource"]
@@ -47,9 +47,9 @@ def with_streaming_response(self) -> DomainsResourceWithStreamingResponse:
def create(
self,
+ tenant_id: str,
*,
name: str,
- tenant_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,
@@ -57,13 +57,12 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainCreateResponse:
- """Add a new domain for sending emails.
+ """Add a new sending domain to a tenant.
- Returns DNS records that must be configured
- before the domain can be verified.
+ Returns DNS records that must be
+ configured before the domain can be verified.
- **Required:** `tenant_id` to specify which tenant the domain belongs to. Each
- tenant gets their own isolated mail server for domain isolation.
+ Each tenant gets their own isolated mail server for domain isolation.
**Required DNS records:**
@@ -71,13 +70,12 @@ def create(
- **DKIM** - TXT record for email signing
- **Return Path** - CNAME for bounce handling
- After adding DNS records, call `POST /domains/{id}/verify` to verify.
+ After adding DNS records, call
+ `POST /tenants/{tenantId}/domains/{domainId}/verify` to verify.
Args:
name: Domain name (e.g., "mail.example.com")
- tenant_id: ID of the tenant this domain belongs to
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -86,15 +84,11 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._post(
- "/domains",
- body=maybe_transform(
- {
- "name": name,
- "tenant_id": tenant_id,
- },
- domain_create_params.DomainCreateParams,
- ),
+ f"/tenants/{tenant_id}/domains",
+ body=maybe_transform({"name": name}, domain_create_params.DomainCreateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -105,6 +99,7 @@ def retrieve(
self,
domain_id: str,
*,
+ tenant_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,
@@ -113,7 +108,7 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainRetrieveResponse:
"""
- Get detailed information about a domain including DNS record status
+ Get detailed information about a domain including DNS record status.
Args:
extra_headers: Send extra headers
@@ -124,10 +119,12 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return self._get(
- f"/domains/{domain_id}",
+ f"/tenants/{tenant_id}/domains/{domain_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -136,8 +133,8 @@ def retrieve(
def list(
self,
+ tenant_id: str,
*,
- tenant_id: 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,
@@ -146,14 +143,9 @@ def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainListResponse:
"""
- Get all sending domains with their verification status.
-
- Optionally filter by `tenant_id` to list domains for a specific tenant. When
- filtered, response includes `tenant_id` and `tenant_name` for each domain.
+ Get all sending domains for a specific tenant with their verification status.
Args:
- tenant_id: Filter domains by tenant ID
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -162,14 +154,12 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._get(
- "/domains",
+ f"/tenants/{tenant_id}/domains",
options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform({"tenant_id": tenant_id}, domain_list_params.DomainListParams),
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=DomainListResponse,
)
@@ -178,6 +168,7 @@ def delete(
self,
domain_id: str,
*,
+ tenant_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,
@@ -185,10 +176,10 @@ def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainDeleteResponse:
- """Remove a sending domain.
+ """Remove a sending domain from a tenant.
- You will no longer be able to send emails from this
- domain.
+ You will no longer be able to send emails
+ from this domain.
**Warning:** This action cannot be undone.
@@ -201,10 +192,12 @@ def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return self._delete(
- f"/domains/{domain_id}",
+ f"/tenants/{tenant_id}/domains/{domain_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -215,6 +208,7 @@ def verify(
self,
domain_id: str,
*,
+ tenant_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,
@@ -238,10 +232,12 @@ def verify(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return self._post(
- f"/domains/{domain_id}/verify",
+ f"/tenants/{tenant_id}/domains/{domain_id}/verify",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -271,9 +267,9 @@ def with_streaming_response(self) -> AsyncDomainsResourceWithStreamingResponse:
async def create(
self,
+ tenant_id: str,
*,
name: str,
- tenant_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,
@@ -281,13 +277,12 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainCreateResponse:
- """Add a new domain for sending emails.
+ """Add a new sending domain to a tenant.
- Returns DNS records that must be configured
- before the domain can be verified.
+ Returns DNS records that must be
+ configured before the domain can be verified.
- **Required:** `tenant_id` to specify which tenant the domain belongs to. Each
- tenant gets their own isolated mail server for domain isolation.
+ Each tenant gets their own isolated mail server for domain isolation.
**Required DNS records:**
@@ -295,13 +290,12 @@ async def create(
- **DKIM** - TXT record for email signing
- **Return Path** - CNAME for bounce handling
- After adding DNS records, call `POST /domains/{id}/verify` to verify.
+ After adding DNS records, call
+ `POST /tenants/{tenantId}/domains/{domainId}/verify` to verify.
Args:
name: Domain name (e.g., "mail.example.com")
- tenant_id: ID of the tenant this domain belongs to
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -310,15 +304,11 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._post(
- "/domains",
- body=await async_maybe_transform(
- {
- "name": name,
- "tenant_id": tenant_id,
- },
- domain_create_params.DomainCreateParams,
- ),
+ f"/tenants/{tenant_id}/domains",
+ body=await async_maybe_transform({"name": name}, domain_create_params.DomainCreateParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -329,6 +319,7 @@ async def retrieve(
self,
domain_id: str,
*,
+ tenant_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,
@@ -337,7 +328,7 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainRetrieveResponse:
"""
- Get detailed information about a domain including DNS record status
+ Get detailed information about a domain including DNS record status.
Args:
extra_headers: Send extra headers
@@ -348,10 +339,12 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return await self._get(
- f"/domains/{domain_id}",
+ f"/tenants/{tenant_id}/domains/{domain_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -360,8 +353,8 @@ async def retrieve(
async def list(
self,
+ tenant_id: str,
*,
- tenant_id: 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,
@@ -370,14 +363,9 @@ async def list(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainListResponse:
"""
- Get all sending domains with their verification status.
-
- Optionally filter by `tenant_id` to list domains for a specific tenant. When
- filtered, response includes `tenant_id` and `tenant_name` for each domain.
+ Get all sending domains for a specific tenant with their verification status.
Args:
- tenant_id: Filter domains by tenant ID
-
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -386,14 +374,12 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._get(
- "/domains",
+ f"/tenants/{tenant_id}/domains",
options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform({"tenant_id": tenant_id}, domain_list_params.DomainListParams),
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=DomainListResponse,
)
@@ -402,6 +388,7 @@ async def delete(
self,
domain_id: str,
*,
+ tenant_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,
@@ -409,10 +396,10 @@ async def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> DomainDeleteResponse:
- """Remove a sending domain.
+ """Remove a sending domain from a tenant.
- You will no longer be able to send emails from this
- domain.
+ You will no longer be able to send emails
+ from this domain.
**Warning:** This action cannot be undone.
@@ -425,10 +412,12 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return await self._delete(
- f"/domains/{domain_id}",
+ f"/tenants/{tenant_id}/domains/{domain_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -439,6 +428,7 @@ async def verify(
self,
domain_id: str,
*,
+ tenant_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,
@@ -462,10 +452,12 @@ async def verify(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not domain_id:
raise ValueError(f"Expected a non-empty value for `domain_id` but received {domain_id!r}")
return await self._post(
- f"/domains/{domain_id}/verify",
+ f"/tenants/{tenant_id}/domains/{domain_id}/verify",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/ark/resources/suppressions.py b/src/ark/resources/tenants/suppressions.py
similarity index 73%
rename from src/ark/resources/suppressions.py
rename to src/ark/resources/tenants/suppressions.py
index 9cfca89..070b7e1 100644
--- a/src/ark/resources/suppressions.py
+++ b/src/ark/resources/tenants/suppressions.py
@@ -2,28 +2,27 @@
from __future__ import annotations
-from typing import Iterable, Optional
+from typing import Optional
import httpx
-from ..types import suppression_list_params, suppression_create_params, suppression_bulk_create_params
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncPageNumberPagination, AsyncPageNumberPagination
-from .._base_client import AsyncPaginator, make_request_options
-from ..types.suppression_list_response import SuppressionListResponse
-from ..types.suppression_create_response import SuppressionCreateResponse
-from ..types.suppression_delete_response import SuppressionDeleteResponse
-from ..types.suppression_retrieve_response import SuppressionRetrieveResponse
-from ..types.suppression_bulk_create_response import SuppressionBulkCreateResponse
+from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.tenants import suppression_list_params, suppression_create_params
+from ...types.tenants.suppression_list_response import SuppressionListResponse
+from ...types.tenants.suppression_create_response import SuppressionCreateResponse
+from ...types.tenants.suppression_delete_response import SuppressionDeleteResponse
+from ...types.tenants.suppression_retrieve_response import SuppressionRetrieveResponse
__all__ = ["SuppressionsResource", "AsyncSuppressionsResource"]
@@ -50,6 +49,7 @@ def with_streaming_response(self) -> SuppressionsResourceWithStreamingResponse:
def create(
self,
+ tenant_id: str,
*,
address: str,
reason: Optional[str] | Omit = omit,
@@ -60,10 +60,10 @@ def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionCreateResponse:
- """Add an email address to the suppression list.
+ """Add an email address to the tenant's suppression list.
- The address will not receive any
- emails until removed.
+ The address will not
+ receive any emails from this tenant until removed.
Args:
address: Email address to suppress
@@ -78,8 +78,10 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._post(
- "/suppressions",
+ f"/tenants/{tenant_id}/suppressions",
body=maybe_transform(
{
"address": address,
@@ -97,6 +99,7 @@ def retrieve(
self,
email: str,
*,
+ tenant_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,
@@ -105,7 +108,7 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionRetrieveResponse:
"""
- Check if a specific email address is on the suppression list
+ Check if a specific email address is on the tenant's suppression list.
Args:
extra_headers: Send extra headers
@@ -116,10 +119,12 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not email:
raise ValueError(f"Expected a non-empty value for `email` but received {email!r}")
return self._get(
- f"/suppressions/{email}",
+ f"/tenants/{tenant_id}/suppressions/{email}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -128,6 +133,7 @@ def retrieve(
def list(
self,
+ tenant_id: str,
*,
page: int | Omit = omit,
per_page: int | Omit = omit,
@@ -138,10 +144,10 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SyncPageNumberPagination[SuppressionListResponse]:
- """Get all email addresses on the suppression list.
+ """Get all email addresses on the tenant's suppression list.
- These addresses will not
- receive any emails.
+ These addresses will
+ not receive any emails from this tenant.
Args:
extra_headers: Send extra headers
@@ -152,8 +158,10 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._get_api_list(
- "/suppressions",
+ f"/tenants/{tenant_id}/suppressions",
page=SyncPageNumberPagination[SuppressionListResponse],
options=make_request_options(
extra_headers=extra_headers,
@@ -175,6 +183,7 @@ def delete(
self,
email: str,
*,
+ tenant_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,
@@ -182,10 +191,10 @@ def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionDeleteResponse:
- """Remove an email address from the suppression list.
+ """Remove an email address from the tenant's suppression list.
- The address will be able to
- receive emails again.
+ The address will be
+ able to receive emails from this tenant again.
Args:
extra_headers: Send extra headers
@@ -196,50 +205,18 @@ def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not email:
raise ValueError(f"Expected a non-empty value for `email` but received {email!r}")
return self._delete(
- f"/suppressions/{email}",
+ f"/tenants/{tenant_id}/suppressions/{email}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=SuppressionDeleteResponse,
)
- def bulk_create(
- self,
- *,
- suppressions: Iterable[suppression_bulk_create_params.Suppression],
- # 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,
- ) -> SuppressionBulkCreateResponse:
- """
- Add up to 1000 email addresses to the suppression list at once
-
- 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
- """
- return self._post(
- "/suppressions/bulk",
- body=maybe_transform(
- {"suppressions": suppressions}, suppression_bulk_create_params.SuppressionBulkCreateParams
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=SuppressionBulkCreateResponse,
- )
-
class AsyncSuppressionsResource(AsyncAPIResource):
@cached_property
@@ -263,6 +240,7 @@ def with_streaming_response(self) -> AsyncSuppressionsResourceWithStreamingRespo
async def create(
self,
+ tenant_id: str,
*,
address: str,
reason: Optional[str] | Omit = omit,
@@ -273,10 +251,10 @@ async def create(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionCreateResponse:
- """Add an email address to the suppression list.
+ """Add an email address to the tenant's suppression list.
- The address will not receive any
- emails until removed.
+ The address will not
+ receive any emails from this tenant until removed.
Args:
address: Email address to suppress
@@ -291,8 +269,10 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._post(
- "/suppressions",
+ f"/tenants/{tenant_id}/suppressions",
body=await async_maybe_transform(
{
"address": address,
@@ -310,6 +290,7 @@ async def retrieve(
self,
email: str,
*,
+ tenant_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,
@@ -318,7 +299,7 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionRetrieveResponse:
"""
- Check if a specific email address is on the suppression list
+ Check if a specific email address is on the tenant's suppression list.
Args:
extra_headers: Send extra headers
@@ -329,10 +310,12 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not email:
raise ValueError(f"Expected a non-empty value for `email` but received {email!r}")
return await self._get(
- f"/suppressions/{email}",
+ f"/tenants/{tenant_id}/suppressions/{email}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -341,6 +324,7 @@ async def retrieve(
def list(
self,
+ tenant_id: str,
*,
page: int | Omit = omit,
per_page: int | Omit = omit,
@@ -351,10 +335,10 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncPaginator[SuppressionListResponse, AsyncPageNumberPagination[SuppressionListResponse]]:
- """Get all email addresses on the suppression list.
+ """Get all email addresses on the tenant's suppression list.
- These addresses will not
- receive any emails.
+ These addresses will
+ not receive any emails from this tenant.
Args:
extra_headers: Send extra headers
@@ -365,8 +349,10 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._get_api_list(
- "/suppressions",
+ f"/tenants/{tenant_id}/suppressions",
page=AsyncPageNumberPagination[SuppressionListResponse],
options=make_request_options(
extra_headers=extra_headers,
@@ -388,6 +374,7 @@ async def delete(
self,
email: str,
*,
+ tenant_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,
@@ -395,10 +382,10 @@ async def delete(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> SuppressionDeleteResponse:
- """Remove an email address from the suppression list.
+ """Remove an email address from the tenant's suppression list.
- The address will be able to
- receive emails again.
+ The address will be
+ able to receive emails from this tenant again.
Args:
extra_headers: Send extra headers
@@ -409,50 +396,18 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not email:
raise ValueError(f"Expected a non-empty value for `email` but received {email!r}")
return await self._delete(
- f"/suppressions/{email}",
+ f"/tenants/{tenant_id}/suppressions/{email}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
cast_to=SuppressionDeleteResponse,
)
- async def bulk_create(
- self,
- *,
- suppressions: Iterable[suppression_bulk_create_params.Suppression],
- # 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,
- ) -> SuppressionBulkCreateResponse:
- """
- Add up to 1000 email addresses to the suppression list at once
-
- 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
- """
- return await self._post(
- "/suppressions/bulk",
- body=await async_maybe_transform(
- {"suppressions": suppressions}, suppression_bulk_create_params.SuppressionBulkCreateParams
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=SuppressionBulkCreateResponse,
- )
-
class SuppressionsResourceWithRawResponse:
def __init__(self, suppressions: SuppressionsResource) -> None:
@@ -470,9 +425,6 @@ def __init__(self, suppressions: SuppressionsResource) -> None:
self.delete = to_raw_response_wrapper(
suppressions.delete,
)
- self.bulk_create = to_raw_response_wrapper(
- suppressions.bulk_create,
- )
class AsyncSuppressionsResourceWithRawResponse:
@@ -491,9 +443,6 @@ def __init__(self, suppressions: AsyncSuppressionsResource) -> None:
self.delete = async_to_raw_response_wrapper(
suppressions.delete,
)
- self.bulk_create = async_to_raw_response_wrapper(
- suppressions.bulk_create,
- )
class SuppressionsResourceWithStreamingResponse:
@@ -512,9 +461,6 @@ def __init__(self, suppressions: SuppressionsResource) -> None:
self.delete = to_streamed_response_wrapper(
suppressions.delete,
)
- self.bulk_create = to_streamed_response_wrapper(
- suppressions.bulk_create,
- )
class AsyncSuppressionsResourceWithStreamingResponse:
@@ -533,6 +479,3 @@ def __init__(self, suppressions: AsyncSuppressionsResource) -> None:
self.delete = async_to_streamed_response_wrapper(
suppressions.delete,
)
- self.bulk_create = async_to_streamed_response_wrapper(
- suppressions.bulk_create,
- )
diff --git a/src/ark/resources/tenants/tenants.py b/src/ark/resources/tenants/tenants.py
index 9c073a5..684cf1a 100644
--- a/src/ark/resources/tenants/tenants.py
+++ b/src/ark/resources/tenants/tenants.py
@@ -7,9 +7,41 @@
import httpx
+from .usage import (
+ UsageResource,
+ AsyncUsageResource,
+ UsageResourceWithRawResponse,
+ AsyncUsageResourceWithRawResponse,
+ UsageResourceWithStreamingResponse,
+ AsyncUsageResourceWithStreamingResponse,
+)
from ...types import tenant_list_params, tenant_create_params, tenant_update_params
+from .domains import (
+ DomainsResource,
+ AsyncDomainsResource,
+ DomainsResourceWithRawResponse,
+ AsyncDomainsResourceWithRawResponse,
+ DomainsResourceWithStreamingResponse,
+ AsyncDomainsResourceWithStreamingResponse,
+)
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from ..._utils import maybe_transform, async_maybe_transform
+from .tracking import (
+ TrackingResource,
+ AsyncTrackingResource,
+ TrackingResourceWithRawResponse,
+ AsyncTrackingResourceWithRawResponse,
+ TrackingResourceWithStreamingResponse,
+ AsyncTrackingResourceWithStreamingResponse,
+)
+from .webhooks import (
+ WebhooksResource,
+ AsyncWebhooksResource,
+ WebhooksResourceWithRawResponse,
+ AsyncWebhooksResourceWithRawResponse,
+ WebhooksResourceWithStreamingResponse,
+ AsyncWebhooksResourceWithStreamingResponse,
+)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -27,6 +59,14 @@
AsyncCredentialsResourceWithStreamingResponse,
)
from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from .suppressions import (
+ SuppressionsResource,
+ AsyncSuppressionsResource,
+ SuppressionsResourceWithRawResponse,
+ AsyncSuppressionsResourceWithRawResponse,
+ SuppressionsResourceWithStreamingResponse,
+ AsyncSuppressionsResourceWithStreamingResponse,
+)
from ..._base_client import AsyncPaginator, make_request_options
from ...types.tenant import Tenant
from ...types.tenant_create_response import TenantCreateResponse
@@ -42,6 +82,26 @@ class TenantsResource(SyncAPIResource):
def credentials(self) -> CredentialsResource:
return CredentialsResource(self._client)
+ @cached_property
+ def domains(self) -> DomainsResource:
+ return DomainsResource(self._client)
+
+ @cached_property
+ def suppressions(self) -> SuppressionsResource:
+ return SuppressionsResource(self._client)
+
+ @cached_property
+ def webhooks(self) -> WebhooksResource:
+ return WebhooksResource(self._client)
+
+ @cached_property
+ def tracking(self) -> TrackingResource:
+ return TrackingResource(self._client)
+
+ @cached_property
+ def usage(self) -> UsageResource:
+ return UsageResource(self._client)
+
@cached_property
def with_raw_response(self) -> TenantsResourceWithRawResponse:
"""
@@ -300,6 +360,26 @@ class AsyncTenantsResource(AsyncAPIResource):
def credentials(self) -> AsyncCredentialsResource:
return AsyncCredentialsResource(self._client)
+ @cached_property
+ def domains(self) -> AsyncDomainsResource:
+ return AsyncDomainsResource(self._client)
+
+ @cached_property
+ def suppressions(self) -> AsyncSuppressionsResource:
+ return AsyncSuppressionsResource(self._client)
+
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResource:
+ return AsyncWebhooksResource(self._client)
+
+ @cached_property
+ def tracking(self) -> AsyncTrackingResource:
+ return AsyncTrackingResource(self._client)
+
+ @cached_property
+ def usage(self) -> AsyncUsageResource:
+ return AsyncUsageResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncTenantsResourceWithRawResponse:
"""
@@ -577,6 +657,26 @@ def __init__(self, tenants: TenantsResource) -> None:
def credentials(self) -> CredentialsResourceWithRawResponse:
return CredentialsResourceWithRawResponse(self._tenants.credentials)
+ @cached_property
+ def domains(self) -> DomainsResourceWithRawResponse:
+ return DomainsResourceWithRawResponse(self._tenants.domains)
+
+ @cached_property
+ def suppressions(self) -> SuppressionsResourceWithRawResponse:
+ return SuppressionsResourceWithRawResponse(self._tenants.suppressions)
+
+ @cached_property
+ def webhooks(self) -> WebhooksResourceWithRawResponse:
+ return WebhooksResourceWithRawResponse(self._tenants.webhooks)
+
+ @cached_property
+ def tracking(self) -> TrackingResourceWithRawResponse:
+ return TrackingResourceWithRawResponse(self._tenants.tracking)
+
+ @cached_property
+ def usage(self) -> UsageResourceWithRawResponse:
+ return UsageResourceWithRawResponse(self._tenants.usage)
+
class AsyncTenantsResourceWithRawResponse:
def __init__(self, tenants: AsyncTenantsResource) -> None:
@@ -602,6 +702,26 @@ def __init__(self, tenants: AsyncTenantsResource) -> None:
def credentials(self) -> AsyncCredentialsResourceWithRawResponse:
return AsyncCredentialsResourceWithRawResponse(self._tenants.credentials)
+ @cached_property
+ def domains(self) -> AsyncDomainsResourceWithRawResponse:
+ return AsyncDomainsResourceWithRawResponse(self._tenants.domains)
+
+ @cached_property
+ def suppressions(self) -> AsyncSuppressionsResourceWithRawResponse:
+ return AsyncSuppressionsResourceWithRawResponse(self._tenants.suppressions)
+
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResourceWithRawResponse:
+ return AsyncWebhooksResourceWithRawResponse(self._tenants.webhooks)
+
+ @cached_property
+ def tracking(self) -> AsyncTrackingResourceWithRawResponse:
+ return AsyncTrackingResourceWithRawResponse(self._tenants.tracking)
+
+ @cached_property
+ def usage(self) -> AsyncUsageResourceWithRawResponse:
+ return AsyncUsageResourceWithRawResponse(self._tenants.usage)
+
class TenantsResourceWithStreamingResponse:
def __init__(self, tenants: TenantsResource) -> None:
@@ -627,6 +747,26 @@ def __init__(self, tenants: TenantsResource) -> None:
def credentials(self) -> CredentialsResourceWithStreamingResponse:
return CredentialsResourceWithStreamingResponse(self._tenants.credentials)
+ @cached_property
+ def domains(self) -> DomainsResourceWithStreamingResponse:
+ return DomainsResourceWithStreamingResponse(self._tenants.domains)
+
+ @cached_property
+ def suppressions(self) -> SuppressionsResourceWithStreamingResponse:
+ return SuppressionsResourceWithStreamingResponse(self._tenants.suppressions)
+
+ @cached_property
+ def webhooks(self) -> WebhooksResourceWithStreamingResponse:
+ return WebhooksResourceWithStreamingResponse(self._tenants.webhooks)
+
+ @cached_property
+ def tracking(self) -> TrackingResourceWithStreamingResponse:
+ return TrackingResourceWithStreamingResponse(self._tenants.tracking)
+
+ @cached_property
+ def usage(self) -> UsageResourceWithStreamingResponse:
+ return UsageResourceWithStreamingResponse(self._tenants.usage)
+
class AsyncTenantsResourceWithStreamingResponse:
def __init__(self, tenants: AsyncTenantsResource) -> None:
@@ -651,3 +791,23 @@ def __init__(self, tenants: AsyncTenantsResource) -> None:
@cached_property
def credentials(self) -> AsyncCredentialsResourceWithStreamingResponse:
return AsyncCredentialsResourceWithStreamingResponse(self._tenants.credentials)
+
+ @cached_property
+ def domains(self) -> AsyncDomainsResourceWithStreamingResponse:
+ return AsyncDomainsResourceWithStreamingResponse(self._tenants.domains)
+
+ @cached_property
+ def suppressions(self) -> AsyncSuppressionsResourceWithStreamingResponse:
+ return AsyncSuppressionsResourceWithStreamingResponse(self._tenants.suppressions)
+
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResourceWithStreamingResponse:
+ return AsyncWebhooksResourceWithStreamingResponse(self._tenants.webhooks)
+
+ @cached_property
+ def tracking(self) -> AsyncTrackingResourceWithStreamingResponse:
+ return AsyncTrackingResourceWithStreamingResponse(self._tenants.tracking)
+
+ @cached_property
+ def usage(self) -> AsyncUsageResourceWithStreamingResponse:
+ return AsyncUsageResourceWithStreamingResponse(self._tenants.usage)
diff --git a/src/ark/resources/tracking.py b/src/ark/resources/tenants/tracking.py
similarity index 84%
rename from src/ark/resources/tracking.py
rename to src/ark/resources/tenants/tracking.py
index ddcbdd5..995292f 100644
--- a/src/ark/resources/tracking.py
+++ b/src/ark/resources/tenants/tracking.py
@@ -6,24 +6,24 @@
import httpx
-from ..types import tracking_create_params, tracking_update_params
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
-from ..types.tracking_list_response import TrackingListResponse
-from ..types.tracking_create_response import TrackingCreateResponse
-from ..types.tracking_delete_response import TrackingDeleteResponse
-from ..types.tracking_update_response import TrackingUpdateResponse
-from ..types.tracking_verify_response import TrackingVerifyResponse
-from ..types.tracking_retrieve_response import TrackingRetrieveResponse
+from ..._base_client import make_request_options
+from ...types.tenants import tracking_create_params, tracking_update_params
+from ...types.tenants.tracking_list_response import TrackingListResponse
+from ...types.tenants.tracking_create_response import TrackingCreateResponse
+from ...types.tenants.tracking_delete_response import TrackingDeleteResponse
+from ...types.tenants.tracking_update_response import TrackingUpdateResponse
+from ...types.tenants.tracking_verify_response import TrackingVerifyResponse
+from ...types.tenants.tracking_retrieve_response import TrackingRetrieveResponse
__all__ = ["TrackingResource", "AsyncTrackingResource"]
@@ -50,6 +50,7 @@ def with_streaming_response(self) -> TrackingResourceWithStreamingResponse:
def create(
self,
+ tenant_id: str,
*,
domain_id: int,
name: str,
@@ -64,7 +65,7 @@ def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingCreateResponse:
"""
- Create a new track domain for open/click tracking.
+ Create a new track domain for open/click tracking for a tenant.
After creation, you must configure a CNAME record pointing to the provided DNS
value before tracking will work.
@@ -88,8 +89,10 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._post(
- "/tracking",
+ f"/tenants/{tenant_id}/tracking",
body=maybe_transform(
{
"domain_id": domain_id,
@@ -110,6 +113,7 @@ def retrieve(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -118,7 +122,7 @@ def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingRetrieveResponse:
"""
- Get details of a specific track domain including DNS configuration
+ Get details of a specific track domain including DNS configuration.
Args:
extra_headers: Send extra headers
@@ -129,10 +133,12 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return self._get(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -143,6 +149,7 @@ def update(
self,
tracking_id: str,
*,
+ tenant_id: str,
excluded_click_domains: Optional[str] | Omit = omit,
ssl_enabled: Optional[bool] | Omit = omit,
track_clicks: Optional[bool] | Omit = omit,
@@ -181,10 +188,12 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return self._patch(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
body=maybe_transform(
{
"excluded_click_domains": excluded_click_domains,
@@ -202,6 +211,7 @@ def update(
def list(
self,
+ tenant_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.
@@ -210,13 +220,24 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingListResponse:
- """List all track domains configured for your server.
+ """List all track domains configured for a tenant.
Track domains enable open and
- click tracking for your emails.
+ click tracking for emails.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._get(
- "/tracking",
+ f"/tenants/{tenant_id}/tracking",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -227,6 +248,7 @@ def delete(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -248,10 +270,12 @@ def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return self._delete(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -262,6 +286,7 @@ def verify(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -284,10 +309,12 @@ def verify(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return self._post(
- f"/tracking/{tracking_id}/verify",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}/verify",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -317,6 +344,7 @@ def with_streaming_response(self) -> AsyncTrackingResourceWithStreamingResponse:
async def create(
self,
+ tenant_id: str,
*,
domain_id: int,
name: str,
@@ -331,7 +359,7 @@ async def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingCreateResponse:
"""
- Create a new track domain for open/click tracking.
+ Create a new track domain for open/click tracking for a tenant.
After creation, you must configure a CNAME record pointing to the provided DNS
value before tracking will work.
@@ -355,8 +383,10 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._post(
- "/tracking",
+ f"/tenants/{tenant_id}/tracking",
body=await async_maybe_transform(
{
"domain_id": domain_id,
@@ -377,6 +407,7 @@ async def retrieve(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -385,7 +416,7 @@ async def retrieve(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingRetrieveResponse:
"""
- Get details of a specific track domain including DNS configuration
+ Get details of a specific track domain including DNS configuration.
Args:
extra_headers: Send extra headers
@@ -396,10 +427,12 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return await self._get(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -410,6 +443,7 @@ async def update(
self,
tracking_id: str,
*,
+ tenant_id: str,
excluded_click_domains: Optional[str] | Omit = omit,
ssl_enabled: Optional[bool] | Omit = omit,
track_clicks: Optional[bool] | Omit = omit,
@@ -448,10 +482,12 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return await self._patch(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
body=await async_maybe_transform(
{
"excluded_click_domains": excluded_click_domains,
@@ -469,6 +505,7 @@ async def update(
async def list(
self,
+ tenant_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.
@@ -477,13 +514,24 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TrackingListResponse:
- """List all track domains configured for your server.
+ """List all track domains configured for a tenant.
Track domains enable open and
- click tracking for your emails.
+ click tracking for emails.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._get(
- "/tracking",
+ f"/tenants/{tenant_id}/tracking",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -494,6 +542,7 @@ async def delete(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -515,10 +564,12 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return await self._delete(
- f"/tracking/{tracking_id}",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -529,6 +580,7 @@ async def verify(
self,
tracking_id: str,
*,
+ tenant_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,
@@ -551,10 +603,12 @@ async def verify(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not tracking_id:
raise ValueError(f"Expected a non-empty value for `tracking_id` but received {tracking_id!r}")
return await self._post(
- f"/tracking/{tracking_id}/verify",
+ f"/tenants/{tenant_id}/tracking/{tracking_id}/verify",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/ark/resources/tenants/usage.py b/src/ark/resources/tenants/usage.py
new file mode 100644
index 0000000..ce82b19
--- /dev/null
+++ b/src/ark/resources/tenants/usage.py
@@ -0,0 +1,402 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import make_request_options
+from ...types.tenants import usage_retrieve_params, usage_retrieve_timeseries_params
+from ...types.tenants.usage_retrieve_response import UsageRetrieveResponse
+from ...types.tenants.usage_retrieve_timeseries_response import UsageRetrieveTimeseriesResponse
+
+__all__ = ["UsageResource", "AsyncUsageResource"]
+
+
+class UsageResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> UsageResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return UsageResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> UsageResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return UsageResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ tenant_id: str,
+ *,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveResponse:
+ """
+ Returns email sending statistics for a specific tenant over a time period.
+
+ **Use cases:**
+
+ - Display usage dashboard to your customers
+ - Calculate per-tenant billing
+ - Monitor tenant health and delivery rates
+
+ **Period formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01` (full month)
+ - Date range: `2024-01-01..2024-01-31`
+ - Single day: `2024-01-15`
+
+ **Response includes:**
+
+ - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
+ - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
+
+ Args:
+ period: Time period for usage data. Defaults to current month.
+
+ **Formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01`
+ - Range: `2024-01-01..2024-01-31`
+ - Day: `2024-01-15`
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/usage",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_params.UsageRetrieveParams,
+ ),
+ ),
+ cast_to=UsageRetrieveResponse,
+ )
+
+ def retrieve_timeseries(
+ self,
+ tenant_id: str,
+ *,
+ granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTimeseriesResponse:
+ """
+ Returns time-bucketed email statistics for a specific tenant.
+
+ **Use cases:**
+
+ - Build usage charts and graphs
+ - Identify sending patterns
+ - Detect anomalies in delivery rates
+
+ **Granularity options:**
+
+ - `hour` - Hourly buckets (best for last 7 days)
+ - `day` - Daily buckets (best for last 30-90 days)
+ - `week` - Weekly buckets (best for last 6 months)
+ - `month` - Monthly buckets (best for year-over-year)
+
+ The response includes a data point for each time bucket with all email metrics.
+
+ Args:
+ granularity: Time bucket size for data points
+
+ period: Time period for timeseries data. Defaults to current month.
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return self._get(
+ f"/tenants/{tenant_id}/usage/timeseries",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "granularity": granularity,
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_timeseries_params.UsageRetrieveTimeseriesParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTimeseriesResponse,
+ )
+
+
+class AsyncUsageResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncUsageResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncUsageResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncUsageResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncUsageResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ tenant_id: str,
+ *,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveResponse:
+ """
+ Returns email sending statistics for a specific tenant over a time period.
+
+ **Use cases:**
+
+ - Display usage dashboard to your customers
+ - Calculate per-tenant billing
+ - Monitor tenant health and delivery rates
+
+ **Period formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01` (full month)
+ - Date range: `2024-01-01..2024-01-31`
+ - Single day: `2024-01-15`
+
+ **Response includes:**
+
+ - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
+ - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
+
+ Args:
+ period: Time period for usage data. Defaults to current month.
+
+ **Formats:**
+
+ - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+ - Month: `2024-01`
+ - Range: `2024-01-01..2024-01-31`
+ - Day: `2024-01-15`
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/usage",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_params.UsageRetrieveParams,
+ ),
+ ),
+ cast_to=UsageRetrieveResponse,
+ )
+
+ async def retrieve_timeseries(
+ self,
+ tenant_id: str,
+ *,
+ granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
+ period: str | Omit = omit,
+ timezone: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> UsageRetrieveTimeseriesResponse:
+ """
+ Returns time-bucketed email statistics for a specific tenant.
+
+ **Use cases:**
+
+ - Build usage charts and graphs
+ - Identify sending patterns
+ - Detect anomalies in delivery rates
+
+ **Granularity options:**
+
+ - `hour` - Hourly buckets (best for last 7 days)
+ - `day` - Daily buckets (best for last 30-90 days)
+ - `week` - Weekly buckets (best for last 6 months)
+ - `month` - Monthly buckets (best for year-over-year)
+
+ The response includes a data point for each time bucket with all email metrics.
+
+ Args:
+ granularity: Time bucket size for data points
+
+ period: Time period for timeseries data. Defaults to current month.
+
+ timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
+ return await self._get(
+ f"/tenants/{tenant_id}/usage/timeseries",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform(
+ {
+ "granularity": granularity,
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_timeseries_params.UsageRetrieveTimeseriesParams,
+ ),
+ ),
+ cast_to=UsageRetrieveTimeseriesResponse,
+ )
+
+
+class UsageResourceWithRawResponse:
+ def __init__(self, usage: UsageResource) -> None:
+ self._usage = usage
+
+ self.retrieve = to_raw_response_wrapper(
+ usage.retrieve,
+ )
+ self.retrieve_timeseries = to_raw_response_wrapper(
+ usage.retrieve_timeseries,
+ )
+
+
+class AsyncUsageResourceWithRawResponse:
+ def __init__(self, usage: AsyncUsageResource) -> None:
+ self._usage = usage
+
+ self.retrieve = async_to_raw_response_wrapper(
+ usage.retrieve,
+ )
+ self.retrieve_timeseries = async_to_raw_response_wrapper(
+ usage.retrieve_timeseries,
+ )
+
+
+class UsageResourceWithStreamingResponse:
+ def __init__(self, usage: UsageResource) -> None:
+ self._usage = usage
+
+ self.retrieve = to_streamed_response_wrapper(
+ usage.retrieve,
+ )
+ self.retrieve_timeseries = to_streamed_response_wrapper(
+ usage.retrieve_timeseries,
+ )
+
+
+class AsyncUsageResourceWithStreamingResponse:
+ def __init__(self, usage: AsyncUsageResource) -> None:
+ self._usage = usage
+
+ self.retrieve = async_to_streamed_response_wrapper(
+ usage.retrieve,
+ )
+ self.retrieve_timeseries = async_to_streamed_response_wrapper(
+ usage.retrieve_timeseries,
+ )
diff --git a/src/ark/resources/webhooks.py b/src/ark/resources/tenants/webhooks.py
similarity index 87%
rename from src/ark/resources/webhooks.py
rename to src/ark/resources/tenants/webhooks.py
index 97b0202..ee3a683 100644
--- a/src/ark/resources/webhooks.py
+++ b/src/ark/resources/tenants/webhooks.py
@@ -7,27 +7,32 @@
import httpx
-from ..types import webhook_test_params, webhook_create_params, webhook_update_params, webhook_list_deliveries_params
-from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
-from .._utils import maybe_transform, async_maybe_transform
-from .._compat import cached_property
-from .._resource import SyncAPIResource, AsyncAPIResource
-from .._response import (
+from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
-from ..types.webhook_list_response import WebhookListResponse
-from ..types.webhook_test_response import WebhookTestResponse
-from ..types.webhook_create_response import WebhookCreateResponse
-from ..types.webhook_delete_response import WebhookDeleteResponse
-from ..types.webhook_update_response import WebhookUpdateResponse
-from ..types.webhook_retrieve_response import WebhookRetrieveResponse
-from ..types.webhook_list_deliveries_response import WebhookListDeliveriesResponse
-from ..types.webhook_replay_delivery_response import WebhookReplayDeliveryResponse
-from ..types.webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse
+from ..._base_client import make_request_options
+from ...types.tenants import (
+ webhook_test_params,
+ webhook_create_params,
+ webhook_update_params,
+ webhook_list_deliveries_params,
+)
+from ...types.tenants.webhook_list_response import WebhookListResponse
+from ...types.tenants.webhook_test_response import WebhookTestResponse
+from ...types.tenants.webhook_create_response import WebhookCreateResponse
+from ...types.tenants.webhook_delete_response import WebhookDeleteResponse
+from ...types.tenants.webhook_update_response import WebhookUpdateResponse
+from ...types.tenants.webhook_retrieve_response import WebhookRetrieveResponse
+from ...types.tenants.webhook_list_deliveries_response import WebhookListDeliveriesResponse
+from ...types.tenants.webhook_replay_delivery_response import WebhookReplayDeliveryResponse
+from ...types.tenants.webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse
__all__ = ["WebhooksResource", "AsyncWebhooksResource"]
@@ -54,6 +59,7 @@ def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse:
def create(
self,
+ tenant_id: str,
*,
name: str,
url: str,
@@ -82,7 +88,7 @@ def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> WebhookCreateResponse:
"""
- Create a webhook endpoint to receive email event notifications.
+ Create a webhook endpoint to receive email event notifications for a tenant.
**Available events:**
@@ -124,8 +130,10 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._post(
- "/webhooks",
+ f"/tenants/{tenant_id}/webhooks",
body=maybe_transform(
{
"name": name,
@@ -146,6 +154,7 @@ def retrieve(
self,
webhook_id: str,
*,
+ tenant_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,
@@ -165,10 +174,12 @@ def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return self._get(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -179,6 +190,7 @@ def update(
self,
webhook_id: str,
*,
+ tenant_id: str,
all_events: Optional[bool] | Omit = omit,
enabled: Optional[bool] | Omit = omit,
events: Optional[SequenceNotStr[str]] | Omit = omit,
@@ -203,10 +215,12 @@ def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return self._patch(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
body=maybe_transform(
{
"all_events": all_events,
@@ -225,6 +239,7 @@ def update(
def list(
self,
+ tenant_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.
@@ -233,9 +248,22 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> WebhookListResponse:
- """Get all configured webhook endpoints"""
+ """
+ Get all configured webhook endpoints for a tenant.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return self._get(
- "/webhooks",
+ f"/tenants/{tenant_id}/webhooks",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -246,6 +274,7 @@ def delete(
self,
webhook_id: str,
*,
+ tenant_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,
@@ -265,10 +294,12 @@ def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return self._delete(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -279,6 +310,7 @@ def list_deliveries(
self,
webhook_id: str,
*,
+ tenant_id: str,
after: int | Omit = omit,
before: int | Omit = omit,
event: Literal[
@@ -341,10 +373,12 @@ def list_deliveries(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return self._get(
- f"/webhooks/{webhook_id}/deliveries",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -369,6 +403,7 @@ def replay_delivery(
self,
delivery_id: str,
*,
+ tenant_id: str,
webhook_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.
@@ -404,12 +439,14 @@ def replay_delivery(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
if not delivery_id:
raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
return self._post(
- f"/webhooks/{webhook_id}/deliveries/{delivery_id}/replay",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -420,6 +457,7 @@ def retrieve_delivery(
self,
delivery_id: str,
*,
+ tenant_id: str,
webhook_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.
@@ -449,12 +487,14 @@ def retrieve_delivery(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
if not delivery_id:
raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
return self._get(
- f"/webhooks/{webhook_id}/deliveries/{delivery_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries/{delivery_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -465,6 +505,7 @@ def test(
self,
webhook_id: str,
*,
+ tenant_id: str,
event: Literal[
"MessageSent",
"MessageDelayed",
@@ -507,10 +548,12 @@ def test(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return self._post(
- f"/webhooks/{webhook_id}/test",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/test",
body=maybe_transform({"event": event}, webhook_test_params.WebhookTestParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -541,6 +584,7 @@ def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse:
async def create(
self,
+ tenant_id: str,
*,
name: str,
url: str,
@@ -569,7 +613,7 @@ async def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> WebhookCreateResponse:
"""
- Create a webhook endpoint to receive email event notifications.
+ Create a webhook endpoint to receive email event notifications for a tenant.
**Available events:**
@@ -611,8 +655,10 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._post(
- "/webhooks",
+ f"/tenants/{tenant_id}/webhooks",
body=await async_maybe_transform(
{
"name": name,
@@ -633,6 +679,7 @@ async def retrieve(
self,
webhook_id: str,
*,
+ tenant_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,
@@ -652,10 +699,12 @@ async def retrieve(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return await self._get(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -666,6 +715,7 @@ async def update(
self,
webhook_id: str,
*,
+ tenant_id: str,
all_events: Optional[bool] | Omit = omit,
enabled: Optional[bool] | Omit = omit,
events: Optional[SequenceNotStr[str]] | Omit = omit,
@@ -690,10 +740,12 @@ async def update(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return await self._patch(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
body=await async_maybe_transform(
{
"all_events": all_events,
@@ -712,6 +764,7 @@ async def update(
async def list(
self,
+ tenant_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.
@@ -720,9 +773,22 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> WebhookListResponse:
- """Get all configured webhook endpoints"""
+ """
+ Get all configured webhook endpoints for a tenant.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
return await self._get(
- "/webhooks",
+ f"/tenants/{tenant_id}/webhooks",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -733,6 +799,7 @@ async def delete(
self,
webhook_id: str,
*,
+ tenant_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,
@@ -752,10 +819,12 @@ async def delete(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return await self._delete(
- f"/webhooks/{webhook_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -766,6 +835,7 @@ async def list_deliveries(
self,
webhook_id: str,
*,
+ tenant_id: str,
after: int | Omit = omit,
before: int | Omit = omit,
event: Literal[
@@ -828,10 +898,12 @@ async def list_deliveries(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return await self._get(
- f"/webhooks/{webhook_id}/deliveries",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -856,6 +928,7 @@ async def replay_delivery(
self,
delivery_id: str,
*,
+ tenant_id: str,
webhook_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.
@@ -891,12 +964,14 @@ async def replay_delivery(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
if not delivery_id:
raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
return await self._post(
- f"/webhooks/{webhook_id}/deliveries/{delivery_id}/replay",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -907,6 +982,7 @@ async def retrieve_delivery(
self,
delivery_id: str,
*,
+ tenant_id: str,
webhook_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.
@@ -936,12 +1012,14 @@ async def retrieve_delivery(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
if not delivery_id:
raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
return await self._get(
- f"/webhooks/{webhook_id}/deliveries/{delivery_id}",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/deliveries/{delivery_id}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -952,6 +1030,7 @@ async def test(
self,
webhook_id: str,
*,
+ tenant_id: str,
event: Literal[
"MessageSent",
"MessageDelayed",
@@ -994,10 +1073,12 @@ async def test(
timeout: Override the client-level default timeout for this request, in seconds
"""
+ if not tenant_id:
+ raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
if not webhook_id:
raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
return await self._post(
- f"/webhooks/{webhook_id}/test",
+ f"/tenants/{tenant_id}/webhooks/{webhook_id}/test",
body=await async_maybe_transform({"event": event}, webhook_test_params.WebhookTestParams),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
diff --git a/src/ark/resources/usage.py b/src/ark/resources/usage.py
index 80acc02..9a38220 100644
--- a/src/ark/resources/usage.py
+++ b/src/ark/resources/usage.py
@@ -2,17 +2,11 @@
from __future__ import annotations
-import typing_extensions
from typing_extensions import Literal
import httpx
-from ..types import (
- usage_export_params,
- usage_list_by_tenant_params,
- usage_retrieve_tenant_usage_params,
- usage_retrieve_tenant_timeseries_params,
-)
+from ..types import usage_export_params, usage_retrieve_params, usage_list_tenants_params
from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
@@ -23,13 +17,11 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..pagination import SyncOffsetPagination, AsyncOffsetPagination
+from ..pagination import SyncPageNumberPagination, AsyncPageNumberPagination
from .._base_client import AsyncPaginator, make_request_options
-from ..types.bulk_tenant_usage import Tenant
+from ..types.org_usage_summary import OrgUsageSummary
+from ..types.tenant_usage_item import TenantUsageItem
from ..types.usage_export_response import UsageExportResponse
-from ..types.usage_retrieve_response import UsageRetrieveResponse
-from ..types.usage_retrieve_tenant_usage_response import UsageRetrieveTenantUsageResponse
-from ..types.usage_retrieve_tenant_timeseries_response import UsageRetrieveTenantTimeseriesResponse
__all__ = ["UsageResource", "AsyncUsageResource"]
@@ -54,60 +46,84 @@ def with_streaming_response(self) -> UsageResourceWithStreamingResponse:
"""
return UsageResourceWithStreamingResponse(self)
- @typing_extensions.deprecated("deprecated")
def retrieve(
self,
*,
+ period: str | Omit = omit,
+ timezone: 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,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UsageRetrieveResponse:
- """
- > **Deprecated:** Use `GET /limits` instead for rate limits and send limits.
- > This endpoint will be removed in a future version.
+ ) -> OrgUsageSummary:
+ """Returns aggregated email sending statistics for your entire organization.
- Returns current usage and limit information for your account.
+ For
+ per-tenant breakdown, use `GET /usage/tenants`.
- This endpoint is designed for:
+ **Use cases:**
- - **AI agents/MCP servers:** Check constraints before planning batch operations
- - **Monitoring dashboards:** Display current usage status
- - **Rate limit awareness:** Know remaining capacity before making requests
+ - Platform dashboards showing org-wide metrics
+ - Quick health check on overall sending
+ - Monitoring total volume and delivery rates
**Response includes:**
- - `rateLimit` - API request rate limit (requests per second)
- - `sendLimit` - Email sending limit (emails per hour)
- - `billing` - Credit balance and auto-recharge configuration
+ - `emails` - Aggregated email counts across all tenants
+ - `rates` - Overall delivery and bounce rates
+ - `tenants` - Tenant count summary (total, active, with activity)
+
+ **Related endpoints:**
+
+ - `GET /usage/tenants` - Paginated usage per tenant
+ - `GET /usage/export` - Export usage data for billing
+ - `GET /tenants/{tenantId}/usage` - Single tenant usage details
+ - `GET /limits` - Rate limits and send limits
+
+ Args:
+ period: Time period for usage data.
+
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+
+ **Month format:** `2024-01` (YYYY-MM)
- **Notes:**
+ **Custom range:** `2024-01-01..2024-01-15`
- - This request counts against your rate limit
- - `sendLimit` may be null if Postal is temporarily unavailable
- - `billing` is null if billing is not configured
- - Send limit resets at the top of each hour
+ timezone: Timezone for period calculations (IANA format)
- **Migration:**
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
- - For rate limits and send limits, use `GET /limits`
- - For per-tenant usage analytics, use `GET /tenants/{tenantId}/usage`
- - For bulk tenant usage, use `GET /usage/by-tenant`
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get(
"/usage",
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "period": period,
+ "timezone": timezone,
+ },
+ usage_retrieve_params.UsageRetrieveParams,
+ ),
),
- cast_to=UsageRetrieveResponse,
+ cast_to=OrgUsageSummary,
)
def export(
self,
*,
- format: Literal["csv", "jsonl", "json"] | Omit = omit,
+ format: Literal["csv", "jsonl"] | Omit = omit,
min_sent: int | Omit = omit,
period: str | Omit = omit,
status: Literal["active", "suspended", "archived"] | Omit = omit,
@@ -119,40 +135,48 @@ def export(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UsageExportResponse:
- """
- Export usage data for all tenants in a format suitable for billing systems.
+ """Export email usage data for all tenants in CSV or JSON Lines format.
- **Use cases:**
+ Designed
+ for billing system integration, data warehousing, and analytics.
+
+ **Jobs to be done:**
- - Import into billing systems (Stripe, Chargebee, etc.)
- - Generate invoices
- - Archive usage data
+ - Import usage data into billing systems (Stripe, Chargebee, etc.)
+ - Load into data warehouses (Snowflake, BigQuery, etc.)
+ - Process in spreadsheets (Excel, Google Sheets)
+ - Feed into BI tools (Looker, Metabase, etc.)
**Export formats:**
- - `csv` - Comma-separated values (default)
- - `jsonl` - JSON Lines (one JSON object per line)
- - `json` - JSON array
+ - `csv` - UTF-8 with BOM for Excel compatibility (default)
+ - `jsonl` - JSON Lines (one JSON object per line, streamable)
- **Response headers:**
+ **CSV columns:** `tenant_id`, `tenant_name`, `external_id`, `status`, `sent`,
+ `delivered`, `soft_failed`, `hard_failed`, `bounced`, `held`, `delivery_rate`,
+ `bounce_rate`, `period_start`, `period_end`
- - `X-Total-Tenants` - Total number of tenants in export
- - `X-Total-Sent` - Total emails sent across all tenants
- - `Content-Disposition` - Suggested filename for download
+ **Response headers:**
- This endpoint returns up to 10,000 tenants per request. For organizations with
- more tenants, use the `/usage/by-tenant` endpoint with pagination.
+ - `Content-Disposition` - Filename for download
+ - `Content-Type` - `text/csv` or `application/x-ndjson`
Args:
format: Export format
min_sent: Only include tenants with at least this many emails sent
- period: Time period for export. Defaults to current month.
+ period: Time period for export.
+
+ **Shortcuts:** `this_month`, `last_month`, `last_30_days`, etc.
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
status: Filter by tenant status
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+ timezone: Timezone for period calculations (IANA format)
extra_headers: Send extra headers
@@ -183,14 +207,25 @@ def export(
cast_to=UsageExportResponse,
)
- def list_by_tenant(
+ def list_tenants(
self,
*,
- limit: int | Omit = omit,
min_sent: int | Omit = omit,
- offset: int | Omit = omit,
+ page: int | Omit = omit,
period: str | Omit = omit,
- sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
+ per_page: int | Omit = omit,
+ sort: Literal[
+ "sent",
+ "-sent",
+ "delivered",
+ "-delivered",
+ "bounce_rate",
+ "-bounce_rate",
+ "delivery_rate",
+ "-delivery_rate",
+ "tenant_name",
+ "-tenant_name",
+ ]
| Omit = omit,
status: Literal["active", "suspended", "archived"] | Omit = omit,
timezone: str | Omit = omit,
@@ -200,39 +235,49 @@ def list_by_tenant(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncOffsetPagination[Tenant]:
- """
- Returns email usage statistics for all tenants in your organization.
+ ) -> SyncPageNumberPagination[TenantUsageItem]:
+ """Returns email usage statistics for all tenants in your organization.
- **Use cases:**
+ Results are
+ paginated with page-based navigation.
+
+ **Jobs to be done:**
- - Generate monthly billing reports
+ - Generate monthly billing invoices per tenant
- Build admin dashboards showing all customer usage
- Identify high-volume or problematic tenants
+ - Track usage against plan limits
**Sorting options:**
- `sent`, `-sent` - Sort by emails sent (ascending/descending)
- `delivered`, `-delivered` - Sort by emails delivered
- `bounce_rate`, `-bounce_rate` - Sort by bounce rate
- - `name`, `-name` - Sort alphabetically by tenant name
+ - `tenant_name`, `-tenant_name` - Sort alphabetically by tenant name
**Filtering:**
- `status` - Filter by tenant status (active, suspended, archived)
- - `min_sent` - Only include tenants with at least N emails sent
+ - `minSent` - Only include tenants with at least N emails sent
- Results are paginated. Use `limit` and `offset` for pagination.
+ **Auto-pagination:** SDKs support iterating over all pages automatically.
Args:
- limit: Maximum number of tenants to return (1-100)
-
min_sent: Only include tenants with at least this many emails sent
- offset: Number of tenants to skip for pagination
+ page: Page number (1-indexed)
period: Time period for usage data. Defaults to current month.
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
+
+ per_page: Results per page (max 100)
+
sort: Sort order for results. Prefix with `-` for descending order.
status: Filter by tenant status
@@ -248,8 +293,8 @@ def list_by_tenant(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/usage/by-tenant",
- page=SyncOffsetPagination[Tenant],
+ "/usage/tenants",
+ page=SyncPageNumberPagination[TenantUsageItem],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -257,91 +302,43 @@ def list_by_tenant(
timeout=timeout,
query=maybe_transform(
{
- "limit": limit,
"min_sent": min_sent,
- "offset": offset,
+ "page": page,
"period": period,
+ "per_page": per_page,
"sort": sort,
"status": status,
"timezone": timezone,
},
- usage_list_by_tenant_params.UsageListByTenantParams,
+ usage_list_tenants_params.UsageListTenantsParams,
),
),
- model=Tenant,
+ model=TenantUsageItem,
)
- def retrieve_tenant_timeseries(
- self,
- tenant_id: str,
- *,
- granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
- period: str | Omit = omit,
- timezone: 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,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UsageRetrieveTenantTimeseriesResponse:
- """
- Returns time-bucketed email statistics for a specific tenant.
-
- **Use cases:**
-
- - Build usage charts and graphs
- - Identify sending patterns
- - Detect anomalies in delivery rates
-
- **Granularity options:**
-
- - `hour` - Hourly buckets (best for last 7 days)
- - `day` - Daily buckets (best for last 30-90 days)
- - `week` - Weekly buckets (best for last 6 months)
- - `month` - Monthly buckets (best for year-over-year)
-
- The response includes a data point for each time bucket with all email metrics.
-
- Args:
- granularity: Time bucket size for data points
- period: Time period for timeseries data. Defaults to current month.
-
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
-
- extra_headers: Send extra headers
+class AsyncUsageResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncUsageResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
- extra_query: Add additional query parameters to the request
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncUsageResourceWithRawResponse(self)
- extra_body: Add additional JSON properties to the request
+ @cached_property
+ def with_streaming_response(self) -> AsyncUsageResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
- timeout: Override the client-level default timeout for this request, in seconds
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
"""
- if not tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return self._get(
- f"/tenants/{tenant_id}/usage/timeseries",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "granularity": granularity,
- "period": period,
- "timezone": timezone,
- },
- usage_retrieve_tenant_timeseries_params.UsageRetrieveTenantTimeseriesParams,
- ),
- ),
- cast_to=UsageRetrieveTenantTimeseriesResponse,
- )
+ return AsyncUsageResourceWithStreamingResponse(self)
- def retrieve_tenant_usage(
+ async def retrieve(
self,
- tenant_id: str,
*,
period: str | Omit = omit,
timezone: str | Omit = omit,
@@ -351,41 +348,42 @@ def retrieve_tenant_usage(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UsageRetrieveTenantUsageResponse:
- """
- Returns email sending statistics for a specific tenant over a time period.
+ ) -> OrgUsageSummary:
+ """Returns aggregated email sending statistics for your entire organization.
+
+ For
+ per-tenant breakdown, use `GET /usage/tenants`.
**Use cases:**
- - Display usage dashboard to your customers
- - Calculate per-tenant billing
- - Monitor tenant health and delivery rates
+ - Platform dashboards showing org-wide metrics
+ - Quick health check on overall sending
+ - Monitoring total volume and delivery rates
- **Period formats:**
+ **Response includes:**
- - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
- `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
- - Month: `2024-01` (full month)
- - Date range: `2024-01-01..2024-01-31`
- - Single day: `2024-01-15`
+ - `emails` - Aggregated email counts across all tenants
+ - `rates` - Overall delivery and bounce rates
+ - `tenants` - Tenant count summary (total, active, with activity)
- **Response includes:**
+ **Related endpoints:**
- - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
- - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
+ - `GET /usage/tenants` - Paginated usage per tenant
+ - `GET /usage/export` - Export usage data for billing
+ - `GET /tenants/{tenantId}/usage` - Single tenant usage details
+ - `GET /limits` - Rate limits and send limits
Args:
- period: Time period for usage data. Defaults to current month.
+ period: Time period for usage data.
- **Formats:**
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
- - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
- `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
- - Month: `2024-01`
- - Range: `2024-01-01..2024-01-31`
- - Day: `2024-01-15`
+ **Month format:** `2024-01` (YYYY-MM)
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+ **Custom range:** `2024-01-01..2024-01-15`
+
+ timezone: Timezone for period calculations (IANA format)
extra_headers: Send extra headers
@@ -395,101 +393,28 @@ def retrieve_tenant_usage(
timeout: Override the client-level default timeout for this request, in seconds
"""
- if not tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return self._get(
- f"/tenants/{tenant_id}/usage",
+ return await self._get(
+ "/usage",
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=maybe_transform(
+ query=await async_maybe_transform(
{
"period": period,
"timezone": timezone,
},
- usage_retrieve_tenant_usage_params.UsageRetrieveTenantUsageParams,
+ usage_retrieve_params.UsageRetrieveParams,
),
),
- cast_to=UsageRetrieveTenantUsageResponse,
- )
-
-
-class AsyncUsageResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncUsageResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
- """
- return AsyncUsageResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncUsageResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
- """
- return AsyncUsageResourceWithStreamingResponse(self)
-
- @typing_extensions.deprecated("deprecated")
- async def retrieve(
- self,
- *,
- # 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,
- ) -> UsageRetrieveResponse:
- """
- > **Deprecated:** Use `GET /limits` instead for rate limits and send limits.
- > This endpoint will be removed in a future version.
-
- Returns current usage and limit information for your account.
-
- This endpoint is designed for:
-
- - **AI agents/MCP servers:** Check constraints before planning batch operations
- - **Monitoring dashboards:** Display current usage status
- - **Rate limit awareness:** Know remaining capacity before making requests
-
- **Response includes:**
-
- - `rateLimit` - API request rate limit (requests per second)
- - `sendLimit` - Email sending limit (emails per hour)
- - `billing` - Credit balance and auto-recharge configuration
-
- **Notes:**
-
- - This request counts against your rate limit
- - `sendLimit` may be null if Postal is temporarily unavailable
- - `billing` is null if billing is not configured
- - Send limit resets at the top of each hour
-
- **Migration:**
-
- - For rate limits and send limits, use `GET /limits`
- - For per-tenant usage analytics, use `GET /tenants/{tenantId}/usage`
- - For bulk tenant usage, use `GET /usage/by-tenant`
- """
- return await self._get(
- "/usage",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=UsageRetrieveResponse,
+ cast_to=OrgUsageSummary,
)
async def export(
self,
*,
- format: Literal["csv", "jsonl", "json"] | Omit = omit,
+ format: Literal["csv", "jsonl"] | Omit = omit,
min_sent: int | Omit = omit,
period: str | Omit = omit,
status: Literal["active", "suspended", "archived"] | Omit = omit,
@@ -501,40 +426,48 @@ async def export(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UsageExportResponse:
- """
- Export usage data for all tenants in a format suitable for billing systems.
+ """Export email usage data for all tenants in CSV or JSON Lines format.
- **Use cases:**
+ Designed
+ for billing system integration, data warehousing, and analytics.
- - Import into billing systems (Stripe, Chargebee, etc.)
- - Generate invoices
- - Archive usage data
+ **Jobs to be done:**
+
+ - Import usage data into billing systems (Stripe, Chargebee, etc.)
+ - Load into data warehouses (Snowflake, BigQuery, etc.)
+ - Process in spreadsheets (Excel, Google Sheets)
+ - Feed into BI tools (Looker, Metabase, etc.)
**Export formats:**
- - `csv` - Comma-separated values (default)
- - `jsonl` - JSON Lines (one JSON object per line)
- - `json` - JSON array
+ - `csv` - UTF-8 with BOM for Excel compatibility (default)
+ - `jsonl` - JSON Lines (one JSON object per line, streamable)
- **Response headers:**
+ **CSV columns:** `tenant_id`, `tenant_name`, `external_id`, `status`, `sent`,
+ `delivered`, `soft_failed`, `hard_failed`, `bounced`, `held`, `delivery_rate`,
+ `bounce_rate`, `period_start`, `period_end`
- - `X-Total-Tenants` - Total number of tenants in export
- - `X-Total-Sent` - Total emails sent across all tenants
- - `Content-Disposition` - Suggested filename for download
+ **Response headers:**
- This endpoint returns up to 10,000 tenants per request. For organizations with
- more tenants, use the `/usage/by-tenant` endpoint with pagination.
+ - `Content-Disposition` - Filename for download
+ - `Content-Type` - `text/csv` or `application/x-ndjson`
Args:
format: Export format
min_sent: Only include tenants with at least this many emails sent
- period: Time period for export. Defaults to current month.
+ period: Time period for export.
+
+ **Shortcuts:** `this_month`, `last_month`, `last_30_days`, etc.
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
status: Filter by tenant status
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
+ timezone: Timezone for period calculations (IANA format)
extra_headers: Send extra headers
@@ -565,14 +498,25 @@ async def export(
cast_to=UsageExportResponse,
)
- def list_by_tenant(
+ def list_tenants(
self,
*,
- limit: int | Omit = omit,
min_sent: int | Omit = omit,
- offset: int | Omit = omit,
+ page: int | Omit = omit,
period: str | Omit = omit,
- sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
+ per_page: int | Omit = omit,
+ sort: Literal[
+ "sent",
+ "-sent",
+ "delivered",
+ "-delivered",
+ "bounce_rate",
+ "-bounce_rate",
+ "delivery_rate",
+ "-delivery_rate",
+ "tenant_name",
+ "-tenant_name",
+ ]
| Omit = omit,
status: Literal["active", "suspended", "archived"] | Omit = omit,
timezone: str | Omit = omit,
@@ -582,39 +526,49 @@ def list_by_tenant(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Tenant, AsyncOffsetPagination[Tenant]]:
- """
- Returns email usage statistics for all tenants in your organization.
+ ) -> AsyncPaginator[TenantUsageItem, AsyncPageNumberPagination[TenantUsageItem]]:
+ """Returns email usage statistics for all tenants in your organization.
- **Use cases:**
+ Results are
+ paginated with page-based navigation.
+
+ **Jobs to be done:**
- - Generate monthly billing reports
+ - Generate monthly billing invoices per tenant
- Build admin dashboards showing all customer usage
- Identify high-volume or problematic tenants
+ - Track usage against plan limits
**Sorting options:**
- `sent`, `-sent` - Sort by emails sent (ascending/descending)
- `delivered`, `-delivered` - Sort by emails delivered
- `bounce_rate`, `-bounce_rate` - Sort by bounce rate
- - `name`, `-name` - Sort alphabetically by tenant name
+ - `tenant_name`, `-tenant_name` - Sort alphabetically by tenant name
**Filtering:**
- `status` - Filter by tenant status (active, suspended, archived)
- - `min_sent` - Only include tenants with at least N emails sent
+ - `minSent` - Only include tenants with at least N emails sent
- Results are paginated. Use `limit` and `offset` for pagination.
+ **Auto-pagination:** SDKs support iterating over all pages automatically.
Args:
- limit: Maximum number of tenants to return (1-100)
-
min_sent: Only include tenants with at least this many emails sent
- offset: Number of tenants to skip for pagination
+ page: Page number (1-indexed)
period: Time period for usage data. Defaults to current month.
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
+
+ per_page: Results per page (max 100)
+
sort: Sort order for results. Prefix with `-` for descending order.
status: Filter by tenant status
@@ -630,8 +584,8 @@ def list_by_tenant(
timeout: Override the client-level default timeout for this request, in seconds
"""
return self._get_api_list(
- "/usage/by-tenant",
- page=AsyncOffsetPagination[Tenant],
+ "/usage/tenants",
+ page=AsyncPageNumberPagination[TenantUsageItem],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -639,162 +593,18 @@ def list_by_tenant(
timeout=timeout,
query=maybe_transform(
{
- "limit": limit,
"min_sent": min_sent,
- "offset": offset,
+ "page": page,
"period": period,
+ "per_page": per_page,
"sort": sort,
"status": status,
"timezone": timezone,
},
- usage_list_by_tenant_params.UsageListByTenantParams,
- ),
- ),
- model=Tenant,
- )
-
- async def retrieve_tenant_timeseries(
- self,
- tenant_id: str,
- *,
- granularity: Literal["hour", "day", "week", "month"] | Omit = omit,
- period: str | Omit = omit,
- timezone: 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,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UsageRetrieveTenantTimeseriesResponse:
- """
- Returns time-bucketed email statistics for a specific tenant.
-
- **Use cases:**
-
- - Build usage charts and graphs
- - Identify sending patterns
- - Detect anomalies in delivery rates
-
- **Granularity options:**
-
- - `hour` - Hourly buckets (best for last 7 days)
- - `day` - Daily buckets (best for last 30-90 days)
- - `week` - Weekly buckets (best for last 6 months)
- - `month` - Monthly buckets (best for year-over-year)
-
- The response includes a data point for each time bucket with all email metrics.
-
- Args:
- granularity: Time bucket size for data points
-
- period: Time period for timeseries data. Defaults to current month.
-
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return await self._get(
- f"/tenants/{tenant_id}/usage/timeseries",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {
- "granularity": granularity,
- "period": period,
- "timezone": timezone,
- },
- usage_retrieve_tenant_timeseries_params.UsageRetrieveTenantTimeseriesParams,
- ),
- ),
- cast_to=UsageRetrieveTenantTimeseriesResponse,
- )
-
- async def retrieve_tenant_usage(
- self,
- tenant_id: str,
- *,
- period: str | Omit = omit,
- timezone: 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,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> UsageRetrieveTenantUsageResponse:
- """
- Returns email sending statistics for a specific tenant over a time period.
-
- **Use cases:**
-
- - Display usage dashboard to your customers
- - Calculate per-tenant billing
- - Monitor tenant health and delivery rates
-
- **Period formats:**
-
- - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
- `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
- - Month: `2024-01` (full month)
- - Date range: `2024-01-01..2024-01-31`
- - Single day: `2024-01-15`
-
- **Response includes:**
-
- - `emails` - Counts for sent, delivered, soft_failed, hard_failed, bounced, held
- - `rates` - Delivery rate and bounce rate as decimals (0.95 = 95%)
-
- Args:
- period: Time period for usage data. Defaults to current month.
-
- **Formats:**
-
- - Shortcuts: `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
- `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
- - Month: `2024-01`
- - Range: `2024-01-01..2024-01-31`
- - Day: `2024-01-15`
-
- timezone: Timezone for period calculations (IANA format). Defaults to UTC.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not tenant_id:
- raise ValueError(f"Expected a non-empty value for `tenant_id` but received {tenant_id!r}")
- return await self._get(
- f"/tenants/{tenant_id}/usage",
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {
- "period": period,
- "timezone": timezone,
- },
- usage_retrieve_tenant_usage_params.UsageRetrieveTenantUsageParams,
+ usage_list_tenants_params.UsageListTenantsParams,
),
),
- cast_to=UsageRetrieveTenantUsageResponse,
+ model=TenantUsageItem,
)
@@ -802,22 +612,14 @@ class UsageResourceWithRawResponse:
def __init__(self, usage: UsageResource) -> None:
self._usage = usage
- self.retrieve = ( # pyright: ignore[reportDeprecated]
- to_raw_response_wrapper(
- usage.retrieve, # pyright: ignore[reportDeprecated],
- )
+ self.retrieve = to_raw_response_wrapper(
+ usage.retrieve,
)
self.export = to_raw_response_wrapper(
usage.export,
)
- self.list_by_tenant = to_raw_response_wrapper(
- usage.list_by_tenant,
- )
- self.retrieve_tenant_timeseries = to_raw_response_wrapper(
- usage.retrieve_tenant_timeseries,
- )
- self.retrieve_tenant_usage = to_raw_response_wrapper(
- usage.retrieve_tenant_usage,
+ self.list_tenants = to_raw_response_wrapper(
+ usage.list_tenants,
)
@@ -825,22 +627,14 @@ class AsyncUsageResourceWithRawResponse:
def __init__(self, usage: AsyncUsageResource) -> None:
self._usage = usage
- self.retrieve = ( # pyright: ignore[reportDeprecated]
- async_to_raw_response_wrapper(
- usage.retrieve, # pyright: ignore[reportDeprecated],
- )
+ self.retrieve = async_to_raw_response_wrapper(
+ usage.retrieve,
)
self.export = async_to_raw_response_wrapper(
usage.export,
)
- self.list_by_tenant = async_to_raw_response_wrapper(
- usage.list_by_tenant,
- )
- self.retrieve_tenant_timeseries = async_to_raw_response_wrapper(
- usage.retrieve_tenant_timeseries,
- )
- self.retrieve_tenant_usage = async_to_raw_response_wrapper(
- usage.retrieve_tenant_usage,
+ self.list_tenants = async_to_raw_response_wrapper(
+ usage.list_tenants,
)
@@ -848,22 +642,14 @@ class UsageResourceWithStreamingResponse:
def __init__(self, usage: UsageResource) -> None:
self._usage = usage
- self.retrieve = ( # pyright: ignore[reportDeprecated]
- to_streamed_response_wrapper(
- usage.retrieve, # pyright: ignore[reportDeprecated],
- )
+ self.retrieve = to_streamed_response_wrapper(
+ usage.retrieve,
)
self.export = to_streamed_response_wrapper(
usage.export,
)
- self.list_by_tenant = to_streamed_response_wrapper(
- usage.list_by_tenant,
- )
- self.retrieve_tenant_timeseries = to_streamed_response_wrapper(
- usage.retrieve_tenant_timeseries,
- )
- self.retrieve_tenant_usage = to_streamed_response_wrapper(
- usage.retrieve_tenant_usage,
+ self.list_tenants = to_streamed_response_wrapper(
+ usage.list_tenants,
)
@@ -871,20 +657,12 @@ class AsyncUsageResourceWithStreamingResponse:
def __init__(self, usage: AsyncUsageResource) -> None:
self._usage = usage
- self.retrieve = ( # pyright: ignore[reportDeprecated]
- async_to_streamed_response_wrapper(
- usage.retrieve, # pyright: ignore[reportDeprecated],
- )
+ self.retrieve = async_to_streamed_response_wrapper(
+ usage.retrieve,
)
self.export = async_to_streamed_response_wrapper(
usage.export,
)
- self.list_by_tenant = async_to_streamed_response_wrapper(
- usage.list_by_tenant,
- )
- self.retrieve_tenant_timeseries = async_to_streamed_response_wrapper(
- usage.retrieve_tenant_timeseries,
- )
- self.retrieve_tenant_usage = async_to_streamed_response_wrapper(
- usage.retrieve_tenant_usage,
+ self.list_tenants = async_to_streamed_response_wrapper(
+ usage.list_tenants,
)
diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py
index f53297c..8d422a6 100644
--- a/src/ark/types/__init__.py
+++ b/src/ark/types/__init__.py
@@ -5,26 +5,20 @@
from .shared import APIMeta as APIMeta
from .tenant import Tenant as Tenant
from .log_entry import LogEntry as LogEntry
-from .dns_record import DNSRecord as DNSRecord
from .email_rates import EmailRates as EmailRates
from .limits_data import LimitsData as LimitsData
from .email_counts import EmailCounts as EmailCounts
-from .tenant_usage import TenantUsage as TenantUsage
-from .track_domain import TrackDomain as TrackDomain
from .usage_period import UsagePeriod as UsagePeriod
from .log_list_params import LogListParams as LogListParams
from .log_entry_detail import LogEntryDetail as LogEntryDetail
-from .bulk_tenant_usage import BulkTenantUsage as BulkTenantUsage
from .email_list_params import EmailListParams as EmailListParams
from .email_send_params import EmailSendParams as EmailSendParams
-from .domain_list_params import DomainListParams as DomainListParams
+from .org_usage_summary import OrgUsageSummary as OrgUsageSummary
+from .tenant_usage_item import TenantUsageItem as TenantUsageItem
from .tenant_list_params import TenantListParams as TenantListParams
from .email_list_response import EmailListResponse as EmailListResponse
from .email_send_response import EmailSendResponse as EmailSendResponse
from .usage_export_params import UsageExportParams as UsageExportParams
-from .webhook_test_params import WebhookTestParams as WebhookTestParams
-from .domain_create_params import DomainCreateParams as DomainCreateParams
-from .domain_list_response import DomainListResponse as DomainListResponse
from .email_retry_response import EmailRetryResponse as EmailRetryResponse
from .tenant_create_params import TenantCreateParams as TenantCreateParams
from .tenant_update_params import TenantUpdateParams as TenantUpdateParams
@@ -32,56 +26,15 @@
from .email_send_raw_params import EmailSendRawParams as EmailSendRawParams
from .log_retrieve_response import LogRetrieveResponse as LogRetrieveResponse
from .usage_export_response import UsageExportResponse as UsageExportResponse
-from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
-from .webhook_list_response import WebhookListResponse as WebhookListResponse
-from .webhook_test_response import WebhookTestResponse as WebhookTestResponse
-from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams
-from .domain_create_response import DomainCreateResponse as DomainCreateResponse
-from .domain_delete_response import DomainDeleteResponse as DomainDeleteResponse
-from .domain_verify_response import DomainVerifyResponse as DomainVerifyResponse
+from .usage_retrieve_params import UsageRetrieveParams as UsageRetrieveParams
from .tenant_create_response import TenantCreateResponse as TenantCreateResponse
from .tenant_delete_response import TenantDeleteResponse as TenantDeleteResponse
from .tenant_update_response import TenantUpdateResponse as TenantUpdateResponse
-from .tracking_create_params import TrackingCreateParams as TrackingCreateParams
-from .tracking_list_response import TrackingListResponse as TrackingListResponse
-from .tracking_update_params import TrackingUpdateParams as TrackingUpdateParams
from .email_retrieve_response import EmailRetrieveResponse as EmailRetrieveResponse
from .email_send_batch_params import EmailSendBatchParams as EmailSendBatchParams
from .email_send_raw_response import EmailSendRawResponse as EmailSendRawResponse
from .limit_retrieve_response import LimitRetrieveResponse as LimitRetrieveResponse
-from .suppression_list_params import SuppressionListParams as SuppressionListParams
-from .tenant_usage_timeseries import TenantUsageTimeseries as TenantUsageTimeseries
-from .usage_retrieve_response import UsageRetrieveResponse as UsageRetrieveResponse
-from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse
-from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
-from .webhook_update_response import WebhookUpdateResponse as WebhookUpdateResponse
-from .domain_retrieve_response import DomainRetrieveResponse as DomainRetrieveResponse
from .tenant_retrieve_response import TenantRetrieveResponse as TenantRetrieveResponse
-from .tracking_create_response import TrackingCreateResponse as TrackingCreateResponse
-from .tracking_delete_response import TrackingDeleteResponse as TrackingDeleteResponse
-from .tracking_update_response import TrackingUpdateResponse as TrackingUpdateResponse
-from .tracking_verify_response import TrackingVerifyResponse as TrackingVerifyResponse
from .email_send_batch_response import EmailSendBatchResponse as EmailSendBatchResponse
-from .suppression_create_params import SuppressionCreateParams as SuppressionCreateParams
-from .suppression_list_response import SuppressionListResponse as SuppressionListResponse
-from .webhook_retrieve_response import WebhookRetrieveResponse as WebhookRetrieveResponse
-from .tracking_retrieve_response import TrackingRetrieveResponse as TrackingRetrieveResponse
-from .suppression_create_response import SuppressionCreateResponse as SuppressionCreateResponse
-from .suppression_delete_response import SuppressionDeleteResponse as SuppressionDeleteResponse
-from .usage_list_by_tenant_params import UsageListByTenantParams as UsageListByTenantParams
-from .suppression_retrieve_response import SuppressionRetrieveResponse as SuppressionRetrieveResponse
-from .suppression_bulk_create_params import SuppressionBulkCreateParams as SuppressionBulkCreateParams
-from .webhook_list_deliveries_params import WebhookListDeliveriesParams as WebhookListDeliveriesParams
-from .suppression_bulk_create_response import SuppressionBulkCreateResponse as SuppressionBulkCreateResponse
-from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
-from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
+from .usage_list_tenants_params import UsageListTenantsParams as UsageListTenantsParams
from .email_retrieve_deliveries_response import EmailRetrieveDeliveriesResponse as EmailRetrieveDeliveriesResponse
-from .usage_retrieve_tenant_usage_params import UsageRetrieveTenantUsageParams as UsageRetrieveTenantUsageParams
-from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
-from .usage_retrieve_tenant_usage_response import UsageRetrieveTenantUsageResponse as UsageRetrieveTenantUsageResponse
-from .usage_retrieve_tenant_timeseries_params import (
- UsageRetrieveTenantTimeseriesParams as UsageRetrieveTenantTimeseriesParams,
-)
-from .usage_retrieve_tenant_timeseries_response import (
- UsageRetrieveTenantTimeseriesResponse as UsageRetrieveTenantTimeseriesResponse,
-)
diff --git a/src/ark/types/bulk_tenant_usage.py b/src/ark/types/bulk_tenant_usage.py
deleted file mode 100644
index eedef65..0000000
--- a/src/ark/types/bulk_tenant_usage.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import List, Optional
-from typing_extensions import Literal
-
-from .._models import BaseModel
-from .email_rates import EmailRates
-from .email_counts import EmailCounts
-from .usage_period import UsagePeriod
-
-__all__ = ["BulkTenantUsage", "Pagination", "Summary", "Tenant"]
-
-
-class Pagination(BaseModel):
- """Pagination information for usage queries"""
-
- has_more: bool
- """Whether more pages are available"""
-
- limit: int
- """Maximum items per page"""
-
- offset: int
- """Number of items skipped"""
-
- total: int
- """Total number of tenants matching the query"""
-
-
-class Summary(BaseModel):
- """Aggregate summary across all tenants"""
-
- total_delivered: int
- """Total emails delivered across all tenants"""
-
- total_sent: int
- """Total emails sent across all tenants"""
-
- total_tenants: int
- """Total number of tenants in the query"""
-
-
-class Tenant(BaseModel):
- """Usage record for a single tenant in bulk response"""
-
- emails: EmailCounts
- """Email delivery counts"""
-
- rates: EmailRates
- """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
-
- status: Literal["active", "suspended", "archived"]
- """Current tenant status"""
-
- tenant_id: str
- """Unique tenant identifier"""
-
- tenant_name: str
- """Tenant display name"""
-
- external_id: Optional[str] = None
- """Your external ID for this tenant"""
-
-
-class BulkTenantUsage(BaseModel):
- """Bulk tenant usage data with pagination"""
-
- pagination: Pagination
- """Pagination information for usage queries"""
-
- period: UsagePeriod
- """Time period for usage data"""
-
- summary: Summary
- """Aggregate summary across all tenants"""
-
- tenants: List[Tenant]
- """Array of tenant usage records"""
diff --git a/src/ark/types/domain_list_params.py b/src/ark/types/domain_list_params.py
deleted file mode 100644
index b0447f0..0000000
--- a/src/ark/types/domain_list_params.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import TypedDict
-
-__all__ = ["DomainListParams"]
-
-
-class DomainListParams(TypedDict, total=False):
- tenant_id: str
- """Filter domains by tenant ID"""
diff --git a/src/ark/types/org_usage_summary.py b/src/ark/types/org_usage_summary.py
new file mode 100644
index 0000000..ef35604
--- /dev/null
+++ b/src/ark/types/org_usage_summary.py
@@ -0,0 +1,47 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .email_rates import EmailRates
+from .email_counts import EmailCounts
+from .usage_period import UsagePeriod
+from .shared.api_meta import APIMeta
+
+__all__ = ["OrgUsageSummary", "Data", "DataTenants"]
+
+
+class DataTenants(BaseModel):
+ active: int
+ """Number of active tenants"""
+
+ total: int
+ """Total number of tenants"""
+
+ with_activity: int = FieldInfo(alias="withActivity")
+ """Number of tenants with sending activity"""
+
+
+class Data(BaseModel):
+ emails: EmailCounts
+ """Email delivery counts"""
+
+ period: UsagePeriod
+ """Time period for usage data"""
+
+ rates: EmailRates
+ """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
+
+ tenants: DataTenants
+
+
+class OrgUsageSummary(BaseModel):
+ """Org-wide usage summary response"""
+
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/suppression_bulk_create_params.py b/src/ark/types/suppression_bulk_create_params.py
deleted file mode 100644
index d0d0754..0000000
--- a/src/ark/types/suppression_bulk_create_params.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Iterable, Optional
-from typing_extensions import Required, TypedDict
-
-__all__ = ["SuppressionBulkCreateParams", "Suppression"]
-
-
-class SuppressionBulkCreateParams(TypedDict, total=False):
- suppressions: Required[Iterable[Suppression]]
-
-
-class Suppression(TypedDict, total=False):
- address: Required[str]
-
- reason: Optional[str]
- """Reason for suppression (accepts null)"""
diff --git a/src/ark/types/suppression_bulk_create_response.py b/src/ark/types/suppression_bulk_create_response.py
deleted file mode 100644
index 4545f32..0000000
--- a/src/ark/types/suppression_bulk_create_response.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
-
-__all__ = ["SuppressionBulkCreateResponse", "Data"]
-
-
-class Data(BaseModel):
- added: int
- """Newly suppressed addresses"""
-
- failed: int
- """Invalid addresses skipped"""
-
- total_requested: int = FieldInfo(alias="totalRequested")
- """Total addresses in request"""
-
- updated: int
- """Already suppressed addresses (updated reason)"""
-
-
-class SuppressionBulkCreateResponse(BaseModel):
- data: Data
-
- meta: APIMeta
-
- success: Literal[True]
diff --git a/src/ark/types/tenant_usage_item.py b/src/ark/types/tenant_usage_item.py
new file mode 100644
index 0000000..d303755
--- /dev/null
+++ b/src/ark/types/tenant_usage_item.py
@@ -0,0 +1,34 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+from .email_rates import EmailRates
+from .email_counts import EmailCounts
+
+__all__ = ["TenantUsageItem"]
+
+
+class TenantUsageItem(BaseModel):
+ """Usage record for a single tenant (camelCase for SDK)"""
+
+ emails: EmailCounts
+ """Email delivery counts"""
+
+ rates: EmailRates
+ """Email delivery rates (as decimals, e.g., 0.95 = 95%)"""
+
+ status: Literal["active", "suspended", "archived"]
+ """Current tenant status"""
+
+ tenant_id: str = FieldInfo(alias="tenantId")
+ """Unique tenant identifier"""
+
+ tenant_name: str = FieldInfo(alias="tenantName")
+ """Tenant display name"""
+
+ external_id: Optional[str] = FieldInfo(alias="externalId", default=None)
+ """Your external ID for this tenant"""
diff --git a/src/ark/types/tenants/__init__.py b/src/ark/types/tenants/__init__.py
index 01a33b1..c52a153 100644
--- a/src/ark/types/tenants/__init__.py
+++ b/src/ark/types/tenants/__init__.py
@@ -2,12 +2,53 @@
from __future__ import annotations
+from .dns_record import DNSRecord as DNSRecord
+from .tenant_usage import TenantUsage as TenantUsage
+from .track_domain import TrackDomain as TrackDomain
+from .webhook_test_params import WebhookTestParams as WebhookTestParams
+from .domain_create_params import DomainCreateParams as DomainCreateParams
+from .domain_list_response import DomainListResponse as DomainListResponse
+from .usage_retrieve_params import UsageRetrieveParams as UsageRetrieveParams
+from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
+from .webhook_list_response import WebhookListResponse as WebhookListResponse
+from .webhook_test_response import WebhookTestResponse as WebhookTestResponse
+from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams
from .credential_list_params import CredentialListParams as CredentialListParams
+from .domain_create_response import DomainCreateResponse as DomainCreateResponse
+from .domain_delete_response import DomainDeleteResponse as DomainDeleteResponse
+from .domain_verify_response import DomainVerifyResponse as DomainVerifyResponse
+from .tracking_create_params import TrackingCreateParams as TrackingCreateParams
+from .tracking_list_response import TrackingListResponse as TrackingListResponse
+from .tracking_update_params import TrackingUpdateParams as TrackingUpdateParams
+from .suppression_list_params import SuppressionListParams as SuppressionListParams
+from .tenant_usage_timeseries import TenantUsageTimeseries as TenantUsageTimeseries
+from .usage_retrieve_response import UsageRetrieveResponse as UsageRetrieveResponse
+from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse
+from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
+from .webhook_update_response import WebhookUpdateResponse as WebhookUpdateResponse
from .credential_create_params import CredentialCreateParams as CredentialCreateParams
from .credential_list_response import CredentialListResponse as CredentialListResponse
from .credential_update_params import CredentialUpdateParams as CredentialUpdateParams
+from .domain_retrieve_response import DomainRetrieveResponse as DomainRetrieveResponse
+from .tracking_create_response import TrackingCreateResponse as TrackingCreateResponse
+from .tracking_delete_response import TrackingDeleteResponse as TrackingDeleteResponse
+from .tracking_update_response import TrackingUpdateResponse as TrackingUpdateResponse
+from .tracking_verify_response import TrackingVerifyResponse as TrackingVerifyResponse
+from .suppression_create_params import SuppressionCreateParams as SuppressionCreateParams
+from .suppression_list_response import SuppressionListResponse as SuppressionListResponse
+from .webhook_retrieve_response import WebhookRetrieveResponse as WebhookRetrieveResponse
from .credential_create_response import CredentialCreateResponse as CredentialCreateResponse
from .credential_delete_response import CredentialDeleteResponse as CredentialDeleteResponse
from .credential_retrieve_params import CredentialRetrieveParams as CredentialRetrieveParams
from .credential_update_response import CredentialUpdateResponse as CredentialUpdateResponse
+from .tracking_retrieve_response import TrackingRetrieveResponse as TrackingRetrieveResponse
+from .suppression_create_response import SuppressionCreateResponse as SuppressionCreateResponse
+from .suppression_delete_response import SuppressionDeleteResponse as SuppressionDeleteResponse
from .credential_retrieve_response import CredentialRetrieveResponse as CredentialRetrieveResponse
+from .suppression_retrieve_response import SuppressionRetrieveResponse as SuppressionRetrieveResponse
+from .webhook_list_deliveries_params import WebhookListDeliveriesParams as WebhookListDeliveriesParams
+from .usage_retrieve_timeseries_params import UsageRetrieveTimeseriesParams as UsageRetrieveTimeseriesParams
+from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
+from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
+from .usage_retrieve_timeseries_response import UsageRetrieveTimeseriesResponse as UsageRetrieveTimeseriesResponse
+from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
diff --git a/src/ark/types/dns_record.py b/src/ark/types/tenants/dns_record.py
similarity index 98%
rename from src/ark/types/dns_record.py
rename to src/ark/types/tenants/dns_record.py
index 775e76e..50d47af 100644
--- a/src/ark/types/dns_record.py
+++ b/src/ark/types/tenants/dns_record.py
@@ -5,7 +5,7 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
__all__ = ["DNSRecord"]
diff --git a/src/ark/types/domain_create_params.py b/src/ark/types/tenants/domain_create_params.py
similarity index 80%
rename from src/ark/types/domain_create_params.py
rename to src/ark/types/tenants/domain_create_params.py
index 338fb37..13a4fcb 100644
--- a/src/ark/types/domain_create_params.py
+++ b/src/ark/types/tenants/domain_create_params.py
@@ -10,6 +10,3 @@
class DomainCreateParams(TypedDict, total=False):
name: Required[str]
"""Domain name (e.g., "mail.example.com")"""
-
- tenant_id: Required[str]
- """ID of the tenant this domain belongs to"""
diff --git a/src/ark/types/domain_create_response.py b/src/ark/types/tenants/domain_create_response.py
similarity index 98%
rename from src/ark/types/domain_create_response.py
rename to src/ark/types/tenants/domain_create_response.py
index f00f9fa..39a6dcd 100644
--- a/src/ark/types/domain_create_response.py
+++ b/src/ark/types/tenants/domain_create_response.py
@@ -6,9 +6,9 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
from .dns_record import DNSRecord
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["DomainCreateResponse", "Data", "DataDNSRecords"]
diff --git a/src/ark/types/domain_delete_response.py b/src/ark/types/tenants/domain_delete_response.py
similarity index 81%
rename from src/ark/types/domain_delete_response.py
rename to src/ark/types/tenants/domain_delete_response.py
index 71f6734..ded7cb7 100644
--- a/src/ark/types/domain_delete_response.py
+++ b/src/ark/types/tenants/domain_delete_response.py
@@ -2,8 +2,8 @@
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["DomainDeleteResponse", "Data"]
diff --git a/src/ark/types/domain_list_response.py b/src/ark/types/tenants/domain_list_response.py
similarity index 92%
rename from src/ark/types/domain_list_response.py
rename to src/ark/types/tenants/domain_list_response.py
index 1ae8a49..b7f328f 100644
--- a/src/ark/types/domain_list_response.py
+++ b/src/ark/types/tenants/domain_list_response.py
@@ -3,8 +3,8 @@
from typing import List, Optional
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["DomainListResponse", "Data", "DataDomain"]
diff --git a/src/ark/types/domain_retrieve_response.py b/src/ark/types/tenants/domain_retrieve_response.py
similarity index 98%
rename from src/ark/types/domain_retrieve_response.py
rename to src/ark/types/tenants/domain_retrieve_response.py
index 7eecf9d..6bb15d2 100644
--- a/src/ark/types/domain_retrieve_response.py
+++ b/src/ark/types/tenants/domain_retrieve_response.py
@@ -6,9 +6,9 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
from .dns_record import DNSRecord
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["DomainRetrieveResponse", "Data", "DataDNSRecords"]
diff --git a/src/ark/types/domain_verify_response.py b/src/ark/types/tenants/domain_verify_response.py
similarity index 98%
rename from src/ark/types/domain_verify_response.py
rename to src/ark/types/tenants/domain_verify_response.py
index a85bcf9..cadeb40 100644
--- a/src/ark/types/domain_verify_response.py
+++ b/src/ark/types/tenants/domain_verify_response.py
@@ -6,9 +6,9 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
from .dns_record import DNSRecord
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["DomainVerifyResponse", "Data", "DataDNSRecords"]
diff --git a/src/ark/types/suppression_create_params.py b/src/ark/types/tenants/suppression_create_params.py
similarity index 100%
rename from src/ark/types/suppression_create_params.py
rename to src/ark/types/tenants/suppression_create_params.py
diff --git a/src/ark/types/suppression_create_response.py b/src/ark/types/tenants/suppression_create_response.py
similarity index 89%
rename from src/ark/types/suppression_create_response.py
rename to src/ark/types/tenants/suppression_create_response.py
index 11b0897..021a7e8 100644
--- a/src/ark/types/suppression_create_response.py
+++ b/src/ark/types/tenants/suppression_create_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["SuppressionCreateResponse", "Data"]
diff --git a/src/ark/types/suppression_delete_response.py b/src/ark/types/tenants/suppression_delete_response.py
similarity index 82%
rename from src/ark/types/suppression_delete_response.py
rename to src/ark/types/tenants/suppression_delete_response.py
index 31e4ccc..a942543 100644
--- a/src/ark/types/suppression_delete_response.py
+++ b/src/ark/types/tenants/suppression_delete_response.py
@@ -2,8 +2,8 @@
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["SuppressionDeleteResponse", "Data"]
diff --git a/src/ark/types/suppression_list_params.py b/src/ark/types/tenants/suppression_list_params.py
similarity index 90%
rename from src/ark/types/suppression_list_params.py
rename to src/ark/types/tenants/suppression_list_params.py
index 71ee20d..57fa8bf 100644
--- a/src/ark/types/suppression_list_params.py
+++ b/src/ark/types/tenants/suppression_list_params.py
@@ -4,7 +4,7 @@
from typing_extensions import Annotated, TypedDict
-from .._utils import PropertyInfo
+from ..._utils import PropertyInfo
__all__ = ["SuppressionListParams"]
diff --git a/src/ark/types/suppression_list_response.py b/src/ark/types/tenants/suppression_list_response.py
similarity index 92%
rename from src/ark/types/suppression_list_response.py
rename to src/ark/types/tenants/suppression_list_response.py
index bf55aa5..7a67cee 100644
--- a/src/ark/types/suppression_list_response.py
+++ b/src/ark/types/tenants/suppression_list_response.py
@@ -5,7 +5,7 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
__all__ = ["SuppressionListResponse"]
diff --git a/src/ark/types/suppression_retrieve_response.py b/src/ark/types/tenants/suppression_retrieve_response.py
similarity index 91%
rename from src/ark/types/suppression_retrieve_response.py
rename to src/ark/types/tenants/suppression_retrieve_response.py
index f20757a..699da56 100644
--- a/src/ark/types/suppression_retrieve_response.py
+++ b/src/ark/types/tenants/suppression_retrieve_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["SuppressionRetrieveResponse", "Data"]
diff --git a/src/ark/types/tenant_usage.py b/src/ark/types/tenants/tenant_usage.py
similarity index 80%
rename from src/ark/types/tenant_usage.py
rename to src/ark/types/tenants/tenant_usage.py
index 690a82a..98101fe 100644
--- a/src/ark/types/tenant_usage.py
+++ b/src/ark/types/tenants/tenant_usage.py
@@ -2,10 +2,10 @@
from typing import Optional
-from .._models import BaseModel
-from .email_rates import EmailRates
-from .email_counts import EmailCounts
-from .usage_period import UsagePeriod
+from ..._models import BaseModel
+from ..email_rates import EmailRates
+from ..email_counts import EmailCounts
+from ..usage_period import UsagePeriod
__all__ = ["TenantUsage"]
diff --git a/src/ark/types/tenant_usage_timeseries.py b/src/ark/types/tenants/tenant_usage_timeseries.py
similarity index 93%
rename from src/ark/types/tenant_usage_timeseries.py
rename to src/ark/types/tenants/tenant_usage_timeseries.py
index 2f71fc1..cfc970d 100644
--- a/src/ark/types/tenant_usage_timeseries.py
+++ b/src/ark/types/tenants/tenant_usage_timeseries.py
@@ -4,8 +4,8 @@
from datetime import datetime
from typing_extensions import Literal
-from .._models import BaseModel
-from .usage_period import UsagePeriod
+from ..._models import BaseModel
+from ..usage_period import UsagePeriod
__all__ = ["TenantUsageTimeseries", "Data"]
diff --git a/src/ark/types/track_domain.py b/src/ark/types/tenants/track_domain.py
similarity index 98%
rename from src/ark/types/track_domain.py
rename to src/ark/types/tenants/track_domain.py
index f2786ea..104d261 100644
--- a/src/ark/types/track_domain.py
+++ b/src/ark/types/tenants/track_domain.py
@@ -6,7 +6,7 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
__all__ = ["TrackDomain", "DNSRecord"]
diff --git a/src/ark/types/tracking_create_params.py b/src/ark/types/tenants/tracking_create_params.py
similarity index 96%
rename from src/ark/types/tracking_create_params.py
rename to src/ark/types/tenants/tracking_create_params.py
index d647b65..1ad766b 100644
--- a/src/ark/types/tracking_create_params.py
+++ b/src/ark/types/tenants/tracking_create_params.py
@@ -5,7 +5,7 @@
from typing import Optional
from typing_extensions import Required, Annotated, TypedDict
-from .._utils import PropertyInfo
+from ..._utils import PropertyInfo
__all__ = ["TrackingCreateParams"]
diff --git a/src/ark/types/tracking_create_response.py b/src/ark/types/tenants/tracking_create_response.py
similarity index 81%
rename from src/ark/types/tracking_create_response.py
rename to src/ark/types/tenants/tracking_create_response.py
index 93cbf43..879b574 100644
--- a/src/ark/types/tracking_create_response.py
+++ b/src/ark/types/tenants/tracking_create_response.py
@@ -2,9 +2,9 @@
from typing_extensions import Literal
-from .._models import BaseModel
+from ..._models import BaseModel
from .track_domain import TrackDomain
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingCreateResponse"]
diff --git a/src/ark/types/tracking_delete_response.py b/src/ark/types/tenants/tracking_delete_response.py
similarity index 81%
rename from src/ark/types/tracking_delete_response.py
rename to src/ark/types/tenants/tracking_delete_response.py
index bd440c9..3a1243e 100644
--- a/src/ark/types/tracking_delete_response.py
+++ b/src/ark/types/tenants/tracking_delete_response.py
@@ -2,8 +2,8 @@
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingDeleteResponse", "Data"]
diff --git a/src/ark/types/tracking_list_response.py b/src/ark/types/tenants/tracking_list_response.py
similarity index 86%
rename from src/ark/types/tracking_list_response.py
rename to src/ark/types/tenants/tracking_list_response.py
index ffd31a8..53bddb7 100644
--- a/src/ark/types/tracking_list_response.py
+++ b/src/ark/types/tenants/tracking_list_response.py
@@ -5,9 +5,9 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
+from ..._models import BaseModel
from .track_domain import TrackDomain
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingListResponse", "Data"]
diff --git a/src/ark/types/tracking_retrieve_response.py b/src/ark/types/tenants/tracking_retrieve_response.py
similarity index 81%
rename from src/ark/types/tracking_retrieve_response.py
rename to src/ark/types/tenants/tracking_retrieve_response.py
index cc974a2..7e39ca9 100644
--- a/src/ark/types/tracking_retrieve_response.py
+++ b/src/ark/types/tenants/tracking_retrieve_response.py
@@ -2,9 +2,9 @@
from typing_extensions import Literal
-from .._models import BaseModel
+from ..._models import BaseModel
from .track_domain import TrackDomain
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingRetrieveResponse"]
diff --git a/src/ark/types/tracking_update_params.py b/src/ark/types/tenants/tracking_update_params.py
similarity index 83%
rename from src/ark/types/tracking_update_params.py
rename to src/ark/types/tenants/tracking_update_params.py
index 3ad46a0..febe059 100644
--- a/src/ark/types/tracking_update_params.py
+++ b/src/ark/types/tenants/tracking_update_params.py
@@ -3,14 +3,16 @@
from __future__ import annotations
from typing import Optional
-from typing_extensions import Annotated, TypedDict
+from typing_extensions import Required, Annotated, TypedDict
-from .._utils import PropertyInfo
+from ..._utils import PropertyInfo
__all__ = ["TrackingUpdateParams"]
class TrackingUpdateParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
excluded_click_domains: Annotated[Optional[str], PropertyInfo(alias="excludedClickDomains")]
"""Comma-separated list of domains to exclude from click tracking (accepts null)"""
diff --git a/src/ark/types/tracking_update_response.py b/src/ark/types/tenants/tracking_update_response.py
similarity index 81%
rename from src/ark/types/tracking_update_response.py
rename to src/ark/types/tenants/tracking_update_response.py
index a1761e5..c06efee 100644
--- a/src/ark/types/tracking_update_response.py
+++ b/src/ark/types/tenants/tracking_update_response.py
@@ -2,9 +2,9 @@
from typing_extensions import Literal
-from .._models import BaseModel
+from ..._models import BaseModel
from .track_domain import TrackDomain
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingUpdateResponse"]
diff --git a/src/ark/types/tracking_verify_response.py b/src/ark/types/tenants/tracking_verify_response.py
similarity index 94%
rename from src/ark/types/tracking_verify_response.py
rename to src/ark/types/tenants/tracking_verify_response.py
index 076afbf..d1f2df9 100644
--- a/src/ark/types/tracking_verify_response.py
+++ b/src/ark/types/tenants/tracking_verify_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["TrackingVerifyResponse", "Data", "DataDNSRecord"]
diff --git a/src/ark/types/usage_retrieve_tenant_usage_params.py b/src/ark/types/tenants/usage_retrieve_params.py
similarity index 84%
rename from src/ark/types/usage_retrieve_tenant_usage_params.py
rename to src/ark/types/tenants/usage_retrieve_params.py
index d6b339f..48c3de3 100644
--- a/src/ark/types/usage_retrieve_tenant_usage_params.py
+++ b/src/ark/types/tenants/usage_retrieve_params.py
@@ -4,10 +4,10 @@
from typing_extensions import TypedDict
-__all__ = ["UsageRetrieveTenantUsageParams"]
+__all__ = ["UsageRetrieveParams"]
-class UsageRetrieveTenantUsageParams(TypedDict, total=False):
+class UsageRetrieveParams(TypedDict, total=False):
period: str
"""Time period for usage data. Defaults to current month.
diff --git a/src/ark/types/usage_retrieve_tenant_usage_response.py b/src/ark/types/tenants/usage_retrieve_response.py
similarity index 65%
rename from src/ark/types/usage_retrieve_tenant_usage_response.py
rename to src/ark/types/tenants/usage_retrieve_response.py
index e528001..d7abac0 100644
--- a/src/ark/types/usage_retrieve_tenant_usage_response.py
+++ b/src/ark/types/tenants/usage_retrieve_response.py
@@ -2,14 +2,14 @@
from typing_extensions import Literal
-from .._models import BaseModel
+from ..._models import BaseModel
from .tenant_usage import TenantUsage
-from .shared.api_meta import APIMeta
+from ..shared.api_meta import APIMeta
-__all__ = ["UsageRetrieveTenantUsageResponse"]
+__all__ = ["UsageRetrieveResponse"]
-class UsageRetrieveTenantUsageResponse(BaseModel):
+class UsageRetrieveResponse(BaseModel):
"""Usage statistics for a single tenant"""
data: TenantUsage
diff --git a/src/ark/types/usage_retrieve_tenant_timeseries_params.py b/src/ark/types/tenants/usage_retrieve_timeseries_params.py
similarity index 79%
rename from src/ark/types/usage_retrieve_tenant_timeseries_params.py
rename to src/ark/types/tenants/usage_retrieve_timeseries_params.py
index 59952b7..c6682b3 100644
--- a/src/ark/types/usage_retrieve_tenant_timeseries_params.py
+++ b/src/ark/types/tenants/usage_retrieve_timeseries_params.py
@@ -4,10 +4,10 @@
from typing_extensions import Literal, TypedDict
-__all__ = ["UsageRetrieveTenantTimeseriesParams"]
+__all__ = ["UsageRetrieveTimeseriesParams"]
-class UsageRetrieveTenantTimeseriesParams(TypedDict, total=False):
+class UsageRetrieveTimeseriesParams(TypedDict, total=False):
granularity: Literal["hour", "day", "week", "month"]
"""Time bucket size for data points"""
diff --git a/src/ark/types/usage_retrieve_tenant_timeseries_response.py b/src/ark/types/tenants/usage_retrieve_timeseries_response.py
similarity index 66%
rename from src/ark/types/usage_retrieve_tenant_timeseries_response.py
rename to src/ark/types/tenants/usage_retrieve_timeseries_response.py
index 2199ee5..74962fb 100644
--- a/src/ark/types/usage_retrieve_tenant_timeseries_response.py
+++ b/src/ark/types/tenants/usage_retrieve_timeseries_response.py
@@ -2,14 +2,14 @@
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
from .tenant_usage_timeseries import TenantUsageTimeseries
-__all__ = ["UsageRetrieveTenantTimeseriesResponse"]
+__all__ = ["UsageRetrieveTimeseriesResponse"]
-class UsageRetrieveTenantTimeseriesResponse(BaseModel):
+class UsageRetrieveTimeseriesResponse(BaseModel):
"""Timeseries usage data for a tenant"""
data: TenantUsageTimeseries
diff --git a/src/ark/types/webhook_create_params.py b/src/ark/types/tenants/webhook_create_params.py
similarity index 97%
rename from src/ark/types/webhook_create_params.py
rename to src/ark/types/tenants/webhook_create_params.py
index 9899826..aa4767e 100644
--- a/src/ark/types/webhook_create_params.py
+++ b/src/ark/types/tenants/webhook_create_params.py
@@ -5,7 +5,7 @@
from typing import List, Optional
from typing_extensions import Literal, Required, Annotated, TypedDict
-from .._utils import PropertyInfo
+from ..._utils import PropertyInfo
__all__ = ["WebhookCreateParams"]
diff --git a/src/ark/types/webhook_create_response.py b/src/ark/types/tenants/webhook_create_response.py
similarity index 93%
rename from src/ark/types/webhook_create_response.py
rename to src/ark/types/tenants/webhook_create_response.py
index d78bb58..b30b19e 100644
--- a/src/ark/types/webhook_create_response.py
+++ b/src/ark/types/tenants/webhook_create_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookCreateResponse", "Data"]
diff --git a/src/ark/types/webhook_delete_response.py b/src/ark/types/tenants/webhook_delete_response.py
similarity index 81%
rename from src/ark/types/webhook_delete_response.py
rename to src/ark/types/tenants/webhook_delete_response.py
index 63e9ff8..aee22ba 100644
--- a/src/ark/types/webhook_delete_response.py
+++ b/src/ark/types/tenants/webhook_delete_response.py
@@ -2,8 +2,8 @@
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookDeleteResponse", "Data"]
diff --git a/src/ark/types/webhook_list_deliveries_params.py b/src/ark/types/tenants/webhook_list_deliveries_params.py
similarity index 83%
rename from src/ark/types/webhook_list_deliveries_params.py
rename to src/ark/types/tenants/webhook_list_deliveries_params.py
index c4048e4..c990a0c 100644
--- a/src/ark/types/webhook_list_deliveries_params.py
+++ b/src/ark/types/tenants/webhook_list_deliveries_params.py
@@ -2,14 +2,16 @@
from __future__ import annotations
-from typing_extensions import Literal, Annotated, TypedDict
+from typing_extensions import Literal, Required, Annotated, TypedDict
-from .._utils import PropertyInfo
+from ..._utils import PropertyInfo
__all__ = ["WebhookListDeliveriesParams"]
class WebhookListDeliveriesParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
after: int
"""Only deliveries after this Unix timestamp"""
diff --git a/src/ark/types/webhook_list_deliveries_response.py b/src/ark/types/tenants/webhook_list_deliveries_response.py
similarity index 96%
rename from src/ark/types/webhook_list_deliveries_response.py
rename to src/ark/types/tenants/webhook_list_deliveries_response.py
index 71a5fe2..ce0b50d 100644
--- a/src/ark/types/webhook_list_deliveries_response.py
+++ b/src/ark/types/tenants/webhook_list_deliveries_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookListDeliveriesResponse", "Data"]
diff --git a/src/ark/types/webhook_list_response.py b/src/ark/types/tenants/webhook_list_response.py
similarity index 87%
rename from src/ark/types/webhook_list_response.py
rename to src/ark/types/tenants/webhook_list_response.py
index 10acdd6..ccd413a 100644
--- a/src/ark/types/webhook_list_response.py
+++ b/src/ark/types/tenants/webhook_list_response.py
@@ -3,8 +3,8 @@
from typing import List
from typing_extensions import Literal
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookListResponse", "Data", "DataWebhook"]
diff --git a/src/ark/types/webhook_replay_delivery_response.py b/src/ark/types/tenants/webhook_replay_delivery_response.py
similarity index 93%
rename from src/ark/types/webhook_replay_delivery_response.py
rename to src/ark/types/tenants/webhook_replay_delivery_response.py
index f3961fd..58c9db6 100644
--- a/src/ark/types/webhook_replay_delivery_response.py
+++ b/src/ark/types/tenants/webhook_replay_delivery_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookReplayDeliveryResponse", "Data"]
diff --git a/src/ark/types/webhook_retrieve_delivery_response.py b/src/ark/types/tenants/webhook_retrieve_delivery_response.py
similarity index 97%
rename from src/ark/types/webhook_retrieve_delivery_response.py
rename to src/ark/types/tenants/webhook_retrieve_delivery_response.py
index efec5c4..f13d5f0 100644
--- a/src/ark/types/webhook_retrieve_delivery_response.py
+++ b/src/ark/types/tenants/webhook_retrieve_delivery_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookRetrieveDeliveryResponse", "Data", "DataRequest", "DataResponse"]
diff --git a/src/ark/types/webhook_retrieve_response.py b/src/ark/types/tenants/webhook_retrieve_response.py
similarity index 93%
rename from src/ark/types/webhook_retrieve_response.py
rename to src/ark/types/tenants/webhook_retrieve_response.py
index 6cb2f2e..d2a9d91 100644
--- a/src/ark/types/webhook_retrieve_response.py
+++ b/src/ark/types/tenants/webhook_retrieve_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookRetrieveResponse", "Data"]
diff --git a/src/ark/types/webhook_test_params.py b/src/ark/types/tenants/webhook_test_params.py
similarity index 75%
rename from src/ark/types/webhook_test_params.py
rename to src/ark/types/tenants/webhook_test_params.py
index 3f91fca..dc43485 100644
--- a/src/ark/types/webhook_test_params.py
+++ b/src/ark/types/tenants/webhook_test_params.py
@@ -2,12 +2,16 @@
from __future__ import annotations
-from typing_extensions import Literal, Required, TypedDict
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
__all__ = ["WebhookTestParams"]
class WebhookTestParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
event: Required[
Literal[
"MessageSent",
diff --git a/src/ark/types/webhook_test_response.py b/src/ark/types/tenants/webhook_test_response.py
similarity index 92%
rename from src/ark/types/webhook_test_response.py
rename to src/ark/types/tenants/webhook_test_response.py
index a51041b..60da1c2 100644
--- a/src/ark/types/webhook_test_response.py
+++ b/src/ark/types/tenants/webhook_test_response.py
@@ -5,8 +5,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookTestResponse", "Data"]
diff --git a/src/ark/types/webhook_update_params.py b/src/ark/types/tenants/webhook_update_params.py
similarity index 67%
rename from src/ark/types/webhook_update_params.py
rename to src/ark/types/tenants/webhook_update_params.py
index 934b579..79587f2 100644
--- a/src/ark/types/webhook_update_params.py
+++ b/src/ark/types/tenants/webhook_update_params.py
@@ -3,15 +3,17 @@
from __future__ import annotations
from typing import Optional
-from typing_extensions import Annotated, TypedDict
+from typing_extensions import Required, Annotated, TypedDict
-from .._types import SequenceNotStr
-from .._utils import PropertyInfo
+from ..._types import SequenceNotStr
+from ..._utils import PropertyInfo
__all__ = ["WebhookUpdateParams"]
class WebhookUpdateParams(TypedDict, total=False):
+ tenant_id: Required[Annotated[str, PropertyInfo(alias="tenantId")]]
+
all_events: Annotated[Optional[bool], PropertyInfo(alias="allEvents")]
enabled: Optional[bool]
diff --git a/src/ark/types/webhook_update_response.py b/src/ark/types/tenants/webhook_update_response.py
similarity index 93%
rename from src/ark/types/webhook_update_response.py
rename to src/ark/types/tenants/webhook_update_response.py
index 4d9a0c6..d98f7d4 100644
--- a/src/ark/types/webhook_update_response.py
+++ b/src/ark/types/tenants/webhook_update_response.py
@@ -6,8 +6,8 @@
from pydantic import Field as FieldInfo
-from .._models import BaseModel
-from .shared.api_meta import APIMeta
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
__all__ = ["WebhookUpdateResponse", "Data"]
diff --git a/src/ark/types/usage_export_params.py b/src/ark/types/usage_export_params.py
index a809124..2d02b6e 100644
--- a/src/ark/types/usage_export_params.py
+++ b/src/ark/types/usage_export_params.py
@@ -2,23 +2,32 @@
from __future__ import annotations
-from typing_extensions import Literal, TypedDict
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._utils import PropertyInfo
__all__ = ["UsageExportParams"]
class UsageExportParams(TypedDict, total=False):
- format: Literal["csv", "jsonl", "json"]
+ format: Literal["csv", "jsonl"]
"""Export format"""
- min_sent: int
+ min_sent: Annotated[int, PropertyInfo(alias="minSent")]
"""Only include tenants with at least this many emails sent"""
period: str
- """Time period for export. Defaults to current month."""
+ """Time period for export.
+
+ **Shortcuts:** `this_month`, `last_month`, `last_30_days`, etc.
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
+ """
status: Literal["active", "suspended", "archived"]
"""Filter by tenant status"""
timezone: str
- """Timezone for period calculations (IANA format). Defaults to UTC."""
+ """Timezone for period calculations (IANA format)"""
diff --git a/src/ark/types/usage_list_by_tenant_params.py b/src/ark/types/usage_list_by_tenant_params.py
deleted file mode 100644
index 0956398..0000000
--- a/src/ark/types/usage_list_by_tenant_params.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Literal, TypedDict
-
-__all__ = ["UsageListByTenantParams"]
-
-
-class UsageListByTenantParams(TypedDict, total=False):
- limit: int
- """Maximum number of tenants to return (1-100)"""
-
- min_sent: int
- """Only include tenants with at least this many emails sent"""
-
- offset: int
- """Number of tenants to skip for pagination"""
-
- period: str
- """Time period for usage data. Defaults to current month."""
-
- sort: Literal["sent", "-sent", "delivered", "-delivered", "bounce_rate", "-bounce_rate", "name", "-name"]
- """Sort order for results. Prefix with `-` for descending order."""
-
- status: Literal["active", "suspended", "archived"]
- """Filter by tenant status"""
-
- timezone: str
- """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_list_tenants_params.py b/src/ark/types/usage_list_tenants_params.py
new file mode 100644
index 0000000..bf4d702
--- /dev/null
+++ b/src/ark/types/usage_list_tenants_params.py
@@ -0,0 +1,51 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["UsageListTenantsParams"]
+
+
+class UsageListTenantsParams(TypedDict, total=False):
+ min_sent: Annotated[int, PropertyInfo(alias="minSent")]
+ """Only include tenants with at least this many emails sent"""
+
+ page: int
+ """Page number (1-indexed)"""
+
+ period: str
+ """Time period for usage data. Defaults to current month.
+
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
+ """
+
+ per_page: Annotated[int, PropertyInfo(alias="perPage")]
+ """Results per page (max 100)"""
+
+ sort: Literal[
+ "sent",
+ "-sent",
+ "delivered",
+ "-delivered",
+ "bounce_rate",
+ "-bounce_rate",
+ "delivery_rate",
+ "-delivery_rate",
+ "tenant_name",
+ "-tenant_name",
+ ]
+ """Sort order for results. Prefix with `-` for descending order."""
+
+ status: Literal["active", "suspended", "archived"]
+ """Filter by tenant status"""
+
+ timezone: str
+ """Timezone for period calculations (IANA format). Defaults to UTC."""
diff --git a/src/ark/types/usage_retrieve_params.py b/src/ark/types/usage_retrieve_params.py
new file mode 100644
index 0000000..89a75ba
--- /dev/null
+++ b/src/ark/types/usage_retrieve_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["UsageRetrieveParams"]
+
+
+class UsageRetrieveParams(TypedDict, total=False):
+ period: str
+ """Time period for usage data.
+
+ **Shortcuts:** `today`, `yesterday`, `this_week`, `last_week`, `this_month`,
+ `last_month`, `last_7_days`, `last_30_days`, `last_90_days`
+
+ **Month format:** `2024-01` (YYYY-MM)
+
+ **Custom range:** `2024-01-01..2024-01-15`
+ """
+
+ timezone: str
+ """Timezone for period calculations (IANA format)"""
diff --git a/src/ark/types/usage_retrieve_response.py b/src/ark/types/usage_retrieve_response.py
deleted file mode 100644
index ce54e61..0000000
--- a/src/ark/types/usage_retrieve_response.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal
-
-from .._models import BaseModel
-from .limits_data import LimitsData
-from .shared.api_meta import APIMeta
-
-__all__ = ["UsageRetrieveResponse"]
-
-
-class UsageRetrieveResponse(BaseModel):
- """Account usage and limits response"""
-
- data: LimitsData
- """Current usage and limit information"""
-
- meta: APIMeta
-
- success: Literal[True]
diff --git a/tests/api_resources/test_domains.py b/tests/api_resources/tenants/test_domains.py
similarity index 62%
rename from tests/api_resources/test_domains.py
rename to tests/api_resources/tenants/test_domains.py
index ff34324..b6d9162 100644
--- a/tests/api_resources/test_domains.py
+++ b/tests/api_resources/tenants/test_domains.py
@@ -8,14 +8,14 @@
import pytest
from ark import Ark, AsyncArk
-from ark.types import (
+from tests.utils import assert_matches_type
+from ark.types.tenants import (
DomainListResponse,
DomainCreateResponse,
DomainDeleteResponse,
DomainVerifyResponse,
DomainRetrieveResponse,
)
-from tests.utils import assert_matches_type
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -25,17 +25,17 @@ class TestDomains:
@parametrize
def test_method_create(self, client: Ark) -> None:
- domain = client.domains.create(
- name="notifications.myapp.com",
+ domain = client.tenants.domains.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
)
assert_matches_type(DomainCreateResponse, domain, path=["response"])
@parametrize
def test_raw_response_create(self, client: Ark) -> None:
- response = client.domains.with_raw_response.create(
- name="notifications.myapp.com",
+ response = client.tenants.domains.with_raw_response.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
)
assert response.is_closed is True
@@ -45,9 +45,9 @@ def test_raw_response_create(self, client: Ark) -> None:
@parametrize
def test_streaming_response_create(self, client: Ark) -> None:
- with client.domains.with_streaming_response.create(
- name="notifications.myapp.com",
+ with client.tenants.domains.with_streaming_response.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -57,17 +57,27 @@ def test_streaming_response_create(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_create(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.domains.with_raw_response.create(
+ tenant_id="",
+ name="notifications.myapp.com",
+ )
+
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- domain = client.domains.retrieve(
- "domainId",
+ domain = client.tenants.domains.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainRetrieveResponse, domain, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.domains.with_raw_response.retrieve(
- "domainId",
+ response = client.tenants.domains.with_raw_response.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -77,8 +87,9 @@ def test_raw_response_retrieve(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.domains.with_streaming_response.retrieve(
- "domainId",
+ with client.tenants.domains.with_streaming_response.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -90,26 +101,30 @@ 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 `tenant_id` but received ''"):
+ client.tenants.domains.with_raw_response.retrieve(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- client.domains.with_raw_response.retrieve(
- "",
+ client.tenants.domains.with_raw_response.retrieve(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_list(self, client: Ark) -> None:
- domain = client.domains.list()
- assert_matches_type(DomainListResponse, domain, path=["response"])
-
- @parametrize
- def test_method_list_with_all_params(self, client: Ark) -> None:
- domain = client.domains.list(
- tenant_id="tenant_id",
+ domain = client.tenants.domains.list(
+ "cm6abc123def456",
)
assert_matches_type(DomainListResponse, domain, path=["response"])
@parametrize
def test_raw_response_list(self, client: Ark) -> None:
- response = client.domains.with_raw_response.list()
+ response = client.tenants.domains.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -118,7 +133,9 @@ def test_raw_response_list(self, client: Ark) -> None:
@parametrize
def test_streaming_response_list(self, client: Ark) -> None:
- with client.domains.with_streaming_response.list() as response:
+ with client.tenants.domains.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -127,17 +144,26 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_list(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.domains.with_raw_response.list(
+ "",
+ )
+
@parametrize
def test_method_delete(self, client: Ark) -> None:
- domain = client.domains.delete(
- "domainId",
+ domain = client.tenants.domains.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainDeleteResponse, domain, path=["response"])
@parametrize
def test_raw_response_delete(self, client: Ark) -> None:
- response = client.domains.with_raw_response.delete(
- "domainId",
+ response = client.tenants.domains.with_raw_response.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -147,8 +173,9 @@ def test_raw_response_delete(self, client: Ark) -> None:
@parametrize
def test_streaming_response_delete(self, client: Ark) -> None:
- with client.domains.with_streaming_response.delete(
- "domainId",
+ with client.tenants.domains.with_streaming_response.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -160,22 +187,31 @@ def test_streaming_response_delete(self, client: Ark) -> None:
@parametrize
def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.domains.with_raw_response.delete(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- client.domains.with_raw_response.delete(
- "",
+ client.tenants.domains.with_raw_response.delete(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_verify(self, client: Ark) -> None:
- domain = client.domains.verify(
- "domainId",
+ domain = client.tenants.domains.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainVerifyResponse, domain, path=["response"])
@parametrize
def test_raw_response_verify(self, client: Ark) -> None:
- response = client.domains.with_raw_response.verify(
- "domainId",
+ response = client.tenants.domains.with_raw_response.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -185,8 +221,9 @@ def test_raw_response_verify(self, client: Ark) -> None:
@parametrize
def test_streaming_response_verify(self, client: Ark) -> None:
- with client.domains.with_streaming_response.verify(
- "domainId",
+ with client.tenants.domains.with_streaming_response.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -198,9 +235,16 @@ def test_streaming_response_verify(self, client: Ark) -> None:
@parametrize
def test_path_params_verify(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.domains.with_raw_response.verify(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- client.domains.with_raw_response.verify(
- "",
+ client.tenants.domains.with_raw_response.verify(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
@@ -211,17 +255,17 @@ class TestAsyncDomains:
@parametrize
async def test_method_create(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.create(
- name="notifications.myapp.com",
+ domain = await async_client.tenants.domains.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
)
assert_matches_type(DomainCreateResponse, domain, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncArk) -> None:
- response = await async_client.domains.with_raw_response.create(
- name="notifications.myapp.com",
+ response = await async_client.tenants.domains.with_raw_response.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
)
assert response.is_closed is True
@@ -231,9 +275,9 @@ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
- async with async_client.domains.with_streaming_response.create(
- name="notifications.myapp.com",
+ async with async_client.tenants.domains.with_streaming_response.create(
tenant_id="cm6abc123def456",
+ name="notifications.myapp.com",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -243,17 +287,27 @@ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.domains.with_raw_response.create(
+ tenant_id="",
+ name="notifications.myapp.com",
+ )
+
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.retrieve(
- "domainId",
+ domain = await async_client.tenants.domains.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainRetrieveResponse, domain, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.domains.with_raw_response.retrieve(
- "domainId",
+ response = await async_client.tenants.domains.with_raw_response.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -263,8 +317,9 @@ 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.domains.with_streaming_response.retrieve(
- "domainId",
+ async with async_client.tenants.domains.with_streaming_response.retrieve(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -276,26 +331,30 @@ 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 `tenant_id` but received ''"):
+ await async_client.tenants.domains.with_raw_response.retrieve(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- await async_client.domains.with_raw_response.retrieve(
- "",
+ await async_client.tenants.domains.with_raw_response.retrieve(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.list()
- assert_matches_type(DomainListResponse, domain, path=["response"])
-
- @parametrize
- async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.list(
- tenant_id="tenant_id",
+ domain = await async_client.tenants.domains.list(
+ "cm6abc123def456",
)
assert_matches_type(DomainListResponse, domain, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncArk) -> None:
- response = await async_client.domains.with_raw_response.list()
+ response = await async_client.tenants.domains.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -304,7 +363,9 @@ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
- async with async_client.domains.with_streaming_response.list() as response:
+ async with async_client.tenants.domains.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -313,17 +374,26 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.domains.with_raw_response.list(
+ "",
+ )
+
@parametrize
async def test_method_delete(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.delete(
- "domainId",
+ domain = await async_client.tenants.domains.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainDeleteResponse, domain, path=["response"])
@parametrize
async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
- response = await async_client.domains.with_raw_response.delete(
- "domainId",
+ response = await async_client.tenants.domains.with_raw_response.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -333,8 +403,9 @@ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
- async with async_client.domains.with_streaming_response.delete(
- "domainId",
+ async with async_client.tenants.domains.with_streaming_response.delete(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -346,22 +417,31 @@ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.domains.with_raw_response.delete(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- await async_client.domains.with_raw_response.delete(
- "",
+ await async_client.tenants.domains.with_raw_response.delete(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_verify(self, async_client: AsyncArk) -> None:
- domain = await async_client.domains.verify(
- "domainId",
+ domain = await async_client.tenants.domains.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(DomainVerifyResponse, domain, path=["response"])
@parametrize
async def test_raw_response_verify(self, async_client: AsyncArk) -> None:
- response = await async_client.domains.with_raw_response.verify(
- "domainId",
+ response = await async_client.tenants.domains.with_raw_response.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -371,8 +451,9 @@ async def test_raw_response_verify(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_verify(self, async_client: AsyncArk) -> None:
- async with async_client.domains.with_streaming_response.verify(
- "domainId",
+ async with async_client.tenants.domains.with_streaming_response.verify(
+ domain_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -384,7 +465,14 @@ async def test_streaming_response_verify(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_verify(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.domains.with_raw_response.verify(
+ domain_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `domain_id` but received ''"):
- await async_client.domains.with_raw_response.verify(
- "",
+ await async_client.tenants.domains.with_raw_response.verify(
+ domain_id="",
+ tenant_id="cm6abc123def456",
)
diff --git a/tests/api_resources/test_suppressions.py b/tests/api_resources/tenants/test_suppressions.py
similarity index 62%
rename from tests/api_resources/test_suppressions.py
rename to tests/api_resources/tenants/test_suppressions.py
index d7c0622..879beca 100644
--- a/tests/api_resources/test_suppressions.py
+++ b/tests/api_resources/tenants/test_suppressions.py
@@ -8,15 +8,14 @@
import pytest
from ark import Ark, AsyncArk
-from ark.types import (
+from tests.utils import assert_matches_type
+from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ark.types.tenants import (
SuppressionListResponse,
SuppressionCreateResponse,
SuppressionDeleteResponse,
SuppressionRetrieveResponse,
- SuppressionBulkCreateResponse,
)
-from tests.utils import assert_matches_type
-from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -26,14 +25,16 @@ class TestSuppressions:
@parametrize
def test_method_create(self, client: Ark) -> None:
- suppression = client.suppressions.create(
+ suppression = client.tenants.suppressions.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
)
assert_matches_type(SuppressionCreateResponse, suppression, path=["response"])
@parametrize
def test_method_create_with_all_params(self, client: Ark) -> None:
- suppression = client.suppressions.create(
+ suppression = client.tenants.suppressions.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
reason="user requested removal",
)
@@ -41,7 +42,8 @@ def test_method_create_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_create(self, client: Ark) -> None:
- response = client.suppressions.with_raw_response.create(
+ response = client.tenants.suppressions.with_raw_response.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
)
@@ -52,7 +54,8 @@ def test_raw_response_create(self, client: Ark) -> None:
@parametrize
def test_streaming_response_create(self, client: Ark) -> None:
- with client.suppressions.with_streaming_response.create(
+ with client.tenants.suppressions.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
) as response:
assert not response.is_closed
@@ -63,17 +66,27 @@ def test_streaming_response_create(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_create(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.suppressions.with_raw_response.create(
+ tenant_id="",
+ address="user@example.com",
+ )
+
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- suppression = client.suppressions.retrieve(
- "dev@stainless.com",
+ suppression = client.tenants.suppressions.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(SuppressionRetrieveResponse, suppression, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.suppressions.with_raw_response.retrieve(
- "dev@stainless.com",
+ response = client.tenants.suppressions.with_raw_response.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -83,8 +96,9 @@ def test_raw_response_retrieve(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.suppressions.with_streaming_response.retrieve(
- "dev@stainless.com",
+ with client.tenants.suppressions.with_streaming_response.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -96,19 +110,29 @@ 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 `tenant_id` but received ''"):
+ client.tenants.suppressions.with_raw_response.retrieve(
+ email="user@example.com",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
- client.suppressions.with_raw_response.retrieve(
- "",
+ client.tenants.suppressions.with_raw_response.retrieve(
+ email="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_list(self, client: Ark) -> None:
- suppression = client.suppressions.list()
+ suppression = client.tenants.suppressions.list(
+ tenant_id="cm6abc123def456",
+ )
assert_matches_type(SyncPageNumberPagination[SuppressionListResponse], suppression, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Ark) -> None:
- suppression = client.suppressions.list(
+ suppression = client.tenants.suppressions.list(
+ tenant_id="cm6abc123def456",
page=0,
per_page=100,
)
@@ -116,7 +140,9 @@ def test_method_list_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_list(self, client: Ark) -> None:
- response = client.suppressions.with_raw_response.list()
+ response = client.tenants.suppressions.with_raw_response.list(
+ tenant_id="cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -125,7 +151,9 @@ def test_raw_response_list(self, client: Ark) -> None:
@parametrize
def test_streaming_response_list(self, client: Ark) -> None:
- with client.suppressions.with_streaming_response.list() as response:
+ with client.tenants.suppressions.with_streaming_response.list(
+ tenant_id="cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -134,17 +162,26 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_list(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.suppressions.with_raw_response.list(
+ tenant_id="",
+ )
+
@parametrize
def test_method_delete(self, client: Ark) -> None:
- suppression = client.suppressions.delete(
- "dev@stainless.com",
+ suppression = client.tenants.suppressions.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(SuppressionDeleteResponse, suppression, path=["response"])
@parametrize
def test_raw_response_delete(self, client: Ark) -> None:
- response = client.suppressions.with_raw_response.delete(
- "dev@stainless.com",
+ response = client.tenants.suppressions.with_raw_response.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -154,8 +191,9 @@ def test_raw_response_delete(self, client: Ark) -> None:
@parametrize
def test_streaming_response_delete(self, client: Ark) -> None:
- with client.suppressions.with_streaming_response.delete(
- "dev@stainless.com",
+ with client.tenants.suppressions.with_streaming_response.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -167,41 +205,17 @@ def test_streaming_response_delete(self, client: Ark) -> None:
@parametrize
def test_path_params_delete(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
- client.suppressions.with_raw_response.delete(
- "",
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.suppressions.with_raw_response.delete(
+ email="user@example.com",
+ tenant_id="",
)
- @parametrize
- def test_method_bulk_create(self, client: Ark) -> None:
- suppression = client.suppressions.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- )
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- @parametrize
- def test_raw_response_bulk_create(self, client: Ark) -> None:
- response = client.suppressions.with_raw_response.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- suppression = response.parse()
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- @parametrize
- def test_streaming_response_bulk_create(self, client: Ark) -> None:
- with client.suppressions.with_streaming_response.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- suppression = response.parse()
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- assert cast(Any, response.is_closed) is True
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
+ client.tenants.suppressions.with_raw_response.delete(
+ email="",
+ tenant_id="cm6abc123def456",
+ )
class TestAsyncSuppressions:
@@ -211,14 +225,16 @@ class TestAsyncSuppressions:
@parametrize
async def test_method_create(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.create(
+ suppression = await async_client.tenants.suppressions.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
)
assert_matches_type(SuppressionCreateResponse, suppression, path=["response"])
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.create(
+ suppression = await async_client.tenants.suppressions.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
reason="user requested removal",
)
@@ -226,7 +242,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> No
@parametrize
async def test_raw_response_create(self, async_client: AsyncArk) -> None:
- response = await async_client.suppressions.with_raw_response.create(
+ response = await async_client.tenants.suppressions.with_raw_response.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
)
@@ -237,7 +254,8 @@ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
- async with async_client.suppressions.with_streaming_response.create(
+ async with async_client.tenants.suppressions.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
address="user@example.com",
) as response:
assert not response.is_closed
@@ -248,17 +266,27 @@ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.suppressions.with_raw_response.create(
+ tenant_id="",
+ address="user@example.com",
+ )
+
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.retrieve(
- "dev@stainless.com",
+ suppression = await async_client.tenants.suppressions.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(SuppressionRetrieveResponse, suppression, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.suppressions.with_raw_response.retrieve(
- "dev@stainless.com",
+ response = await async_client.tenants.suppressions.with_raw_response.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -268,8 +296,9 @@ 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.suppressions.with_streaming_response.retrieve(
- "dev@stainless.com",
+ async with async_client.tenants.suppressions.with_streaming_response.retrieve(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -281,19 +310,29 @@ 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 `tenant_id` but received ''"):
+ await async_client.tenants.suppressions.with_raw_response.retrieve(
+ email="user@example.com",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
- await async_client.suppressions.with_raw_response.retrieve(
- "",
+ await async_client.tenants.suppressions.with_raw_response.retrieve(
+ email="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.list()
+ suppression = await async_client.tenants.suppressions.list(
+ tenant_id="cm6abc123def456",
+ )
assert_matches_type(AsyncPageNumberPagination[SuppressionListResponse], suppression, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.list(
+ suppression = await async_client.tenants.suppressions.list(
+ tenant_id="cm6abc123def456",
page=0,
per_page=100,
)
@@ -301,7 +340,9 @@ async def test_method_list_with_all_params(self, async_client: AsyncArk) -> None
@parametrize
async def test_raw_response_list(self, async_client: AsyncArk) -> None:
- response = await async_client.suppressions.with_raw_response.list()
+ response = await async_client.tenants.suppressions.with_raw_response.list(
+ tenant_id="cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -310,7 +351,9 @@ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
- async with async_client.suppressions.with_streaming_response.list() as response:
+ async with async_client.tenants.suppressions.with_streaming_response.list(
+ tenant_id="cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -319,17 +362,26 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.suppressions.with_raw_response.list(
+ tenant_id="",
+ )
+
@parametrize
async def test_method_delete(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.delete(
- "dev@stainless.com",
+ suppression = await async_client.tenants.suppressions.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(SuppressionDeleteResponse, suppression, path=["response"])
@parametrize
async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
- response = await async_client.suppressions.with_raw_response.delete(
- "dev@stainless.com",
+ response = await async_client.tenants.suppressions.with_raw_response.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -339,8 +391,9 @@ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
- async with async_client.suppressions.with_streaming_response.delete(
- "dev@stainless.com",
+ async with async_client.tenants.suppressions.with_streaming_response.delete(
+ email="user@example.com",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -352,38 +405,14 @@ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_delete(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
- await async_client.suppressions.with_raw_response.delete(
- "",
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.suppressions.with_raw_response.delete(
+ email="user@example.com",
+ tenant_id="",
)
- @parametrize
- async def test_method_bulk_create(self, async_client: AsyncArk) -> None:
- suppression = await async_client.suppressions.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- )
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- @parametrize
- async def test_raw_response_bulk_create(self, async_client: AsyncArk) -> None:
- response = await async_client.suppressions.with_raw_response.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- suppression = await response.parse()
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- @parametrize
- async def test_streaming_response_bulk_create(self, async_client: AsyncArk) -> None:
- async with async_client.suppressions.with_streaming_response.bulk_create(
- suppressions=[{"address": "dev@stainless.com"}],
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- suppression = await response.parse()
- assert_matches_type(SuppressionBulkCreateResponse, suppression, path=["response"])
-
- assert cast(Any, response.is_closed) is True
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `email` but received ''"):
+ await async_client.tenants.suppressions.with_raw_response.delete(
+ email="",
+ tenant_id="cm6abc123def456",
+ )
diff --git a/tests/api_resources/test_tracking.py b/tests/api_resources/tenants/test_tracking.py
similarity index 61%
rename from tests/api_resources/test_tracking.py
rename to tests/api_resources/tenants/test_tracking.py
index 7cd008b..58fb912 100644
--- a/tests/api_resources/test_tracking.py
+++ b/tests/api_resources/tenants/test_tracking.py
@@ -8,7 +8,8 @@
import pytest
from ark import Ark, AsyncArk
-from ark.types import (
+from tests.utils import assert_matches_type
+from ark.types.tenants import (
TrackingListResponse,
TrackingCreateResponse,
TrackingDeleteResponse,
@@ -16,7 +17,6 @@
TrackingVerifyResponse,
TrackingRetrieveResponse,
)
-from tests.utils import assert_matches_type
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -26,7 +26,8 @@ class TestTracking:
@parametrize
def test_method_create(self, client: Ark) -> None:
- tracking = client.tracking.create(
+ tracking = client.tenants.tracking.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
)
@@ -34,7 +35,8 @@ def test_method_create(self, client: Ark) -> None:
@parametrize
def test_method_create_with_all_params(self, client: Ark) -> None:
- tracking = client.tracking.create(
+ tracking = client.tenants.tracking.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
ssl_enabled=True,
@@ -45,7 +47,8 @@ def test_method_create_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_create(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.create(
+ response = client.tenants.tracking.with_raw_response.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
)
@@ -57,7 +60,8 @@ def test_raw_response_create(self, client: Ark) -> None:
@parametrize
def test_streaming_response_create(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.create(
+ with client.tenants.tracking.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
) as response:
@@ -69,17 +73,28 @@ def test_streaming_response_create(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_create(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.create(
+ tenant_id="",
+ domain_id=123,
+ name="track",
+ )
+
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- tracking = client.tracking.retrieve(
- "trackingId",
+ tracking = client.tenants.tracking.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingRetrieveResponse, tracking, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.retrieve(
- "trackingId",
+ response = client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -89,8 +104,9 @@ def test_raw_response_retrieve(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.retrieve(
- "trackingId",
+ with client.tenants.tracking.with_streaming_response.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -102,22 +118,31 @@ 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 `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- client.tracking.with_raw_response.retrieve(
- "",
+ client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_update(self, client: Ark) -> None:
- tracking = client.tracking.update(
- tracking_id="trackingId",
+ tracking = client.tenants.tracking.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingUpdateResponse, tracking, path=["response"])
@parametrize
def test_method_update_with_all_params(self, client: Ark) -> None:
- tracking = client.tracking.update(
- tracking_id="trackingId",
+ tracking = client.tenants.tracking.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
excluded_click_domains="example.com,mysite.org",
ssl_enabled=True,
track_clicks=True,
@@ -127,8 +152,9 @@ def test_method_update_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_update(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.update(
- tracking_id="trackingId",
+ response = client.tenants.tracking.with_raw_response.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -138,8 +164,9 @@ def test_raw_response_update(self, client: Ark) -> None:
@parametrize
def test_streaming_response_update(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.update(
- tracking_id="trackingId",
+ with client.tenants.tracking.with_streaming_response.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -151,19 +178,30 @@ def test_streaming_response_update(self, client: Ark) -> None:
@parametrize
def test_path_params_update(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.update(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- client.tracking.with_raw_response.update(
+ client.tenants.tracking.with_raw_response.update(
tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_list(self, client: Ark) -> None:
- tracking = client.tracking.list()
+ tracking = client.tenants.tracking.list(
+ "cm6abc123def456",
+ )
assert_matches_type(TrackingListResponse, tracking, path=["response"])
@parametrize
def test_raw_response_list(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.list()
+ response = client.tenants.tracking.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -172,7 +210,9 @@ def test_raw_response_list(self, client: Ark) -> None:
@parametrize
def test_streaming_response_list(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.list() as response:
+ with client.tenants.tracking.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -181,17 +221,26 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_list(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.list(
+ "",
+ )
+
@parametrize
def test_method_delete(self, client: Ark) -> None:
- tracking = client.tracking.delete(
- "trackingId",
+ tracking = client.tenants.tracking.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingDeleteResponse, tracking, path=["response"])
@parametrize
def test_raw_response_delete(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.delete(
- "trackingId",
+ response = client.tenants.tracking.with_raw_response.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -201,8 +250,9 @@ def test_raw_response_delete(self, client: Ark) -> None:
@parametrize
def test_streaming_response_delete(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.delete(
- "trackingId",
+ with client.tenants.tracking.with_streaming_response.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -214,22 +264,31 @@ def test_streaming_response_delete(self, client: Ark) -> None:
@parametrize
def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.delete(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- client.tracking.with_raw_response.delete(
- "",
+ client.tenants.tracking.with_raw_response.delete(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_verify(self, client: Ark) -> None:
- tracking = client.tracking.verify(
- "trackingId",
+ tracking = client.tenants.tracking.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingVerifyResponse, tracking, path=["response"])
@parametrize
def test_raw_response_verify(self, client: Ark) -> None:
- response = client.tracking.with_raw_response.verify(
- "trackingId",
+ response = client.tenants.tracking.with_raw_response.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -239,8 +298,9 @@ def test_raw_response_verify(self, client: Ark) -> None:
@parametrize
def test_streaming_response_verify(self, client: Ark) -> None:
- with client.tracking.with_streaming_response.verify(
- "trackingId",
+ with client.tenants.tracking.with_streaming_response.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -252,9 +312,16 @@ def test_streaming_response_verify(self, client: Ark) -> None:
@parametrize
def test_path_params_verify(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.tracking.with_raw_response.verify(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- client.tracking.with_raw_response.verify(
- "",
+ client.tenants.tracking.with_raw_response.verify(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
@@ -265,7 +332,8 @@ class TestAsyncTracking:
@parametrize
async def test_method_create(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.create(
+ tracking = await async_client.tenants.tracking.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
)
@@ -273,7 +341,8 @@ async def test_method_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.create(
+ tracking = await async_client.tenants.tracking.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
ssl_enabled=True,
@@ -284,7 +353,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> No
@parametrize
async def test_raw_response_create(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.create(
+ response = await async_client.tenants.tracking.with_raw_response.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
)
@@ -296,7 +366,8 @@ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
- async with async_client.tracking.with_streaming_response.create(
+ async with async_client.tenants.tracking.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
domain_id=123,
name="track",
) as response:
@@ -308,17 +379,28 @@ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.create(
+ tenant_id="",
+ domain_id=123,
+ name="track",
+ )
+
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.retrieve(
- "trackingId",
+ tracking = await async_client.tenants.tracking.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingRetrieveResponse, tracking, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.retrieve(
- "trackingId",
+ response = await async_client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -328,8 +410,9 @@ 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.tracking.with_streaming_response.retrieve(
- "trackingId",
+ async with async_client.tenants.tracking.with_streaming_response.retrieve(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -341,22 +424,31 @@ 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 `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- await async_client.tracking.with_raw_response.retrieve(
- "",
+ await async_client.tenants.tracking.with_raw_response.retrieve(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_update(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.update(
- tracking_id="trackingId",
+ tracking = await async_client.tenants.tracking.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingUpdateResponse, tracking, path=["response"])
@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.update(
- tracking_id="trackingId",
+ tracking = await async_client.tenants.tracking.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
excluded_click_domains="example.com,mysite.org",
ssl_enabled=True,
track_clicks=True,
@@ -366,8 +458,9 @@ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> No
@parametrize
async def test_raw_response_update(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.update(
- tracking_id="trackingId",
+ response = await async_client.tenants.tracking.with_raw_response.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -377,8 +470,9 @@ async def test_raw_response_update(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
- async with async_client.tracking.with_streaming_response.update(
- tracking_id="trackingId",
+ async with async_client.tenants.tracking.with_streaming_response.update(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -390,19 +484,30 @@ async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_update(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.update(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- await async_client.tracking.with_raw_response.update(
+ await async_client.tenants.tracking.with_raw_response.update(
tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.list()
+ tracking = await async_client.tenants.tracking.list(
+ "cm6abc123def456",
+ )
assert_matches_type(TrackingListResponse, tracking, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.list()
+ response = await async_client.tenants.tracking.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -411,7 +516,9 @@ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
- async with async_client.tracking.with_streaming_response.list() as response:
+ async with async_client.tenants.tracking.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -420,17 +527,26 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.list(
+ "",
+ )
+
@parametrize
async def test_method_delete(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.delete(
- "trackingId",
+ tracking = await async_client.tenants.tracking.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingDeleteResponse, tracking, path=["response"])
@parametrize
async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.delete(
- "trackingId",
+ response = await async_client.tenants.tracking.with_raw_response.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -440,8 +556,9 @@ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
- async with async_client.tracking.with_streaming_response.delete(
- "trackingId",
+ async with async_client.tenants.tracking.with_streaming_response.delete(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -453,22 +570,31 @@ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.delete(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- await async_client.tracking.with_raw_response.delete(
- "",
+ await async_client.tenants.tracking.with_raw_response.delete(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_verify(self, async_client: AsyncArk) -> None:
- tracking = await async_client.tracking.verify(
- "trackingId",
+ tracking = await async_client.tenants.tracking.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(TrackingVerifyResponse, tracking, path=["response"])
@parametrize
async def test_raw_response_verify(self, async_client: AsyncArk) -> None:
- response = await async_client.tracking.with_raw_response.verify(
- "trackingId",
+ response = await async_client.tenants.tracking.with_raw_response.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -478,8 +604,9 @@ async def test_raw_response_verify(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_verify(self, async_client: AsyncArk) -> None:
- async with async_client.tracking.with_streaming_response.verify(
- "trackingId",
+ async with async_client.tenants.tracking.with_streaming_response.verify(
+ tracking_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -491,7 +618,14 @@ async def test_streaming_response_verify(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_verify(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.tracking.with_raw_response.verify(
+ tracking_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `tracking_id` but received ''"):
- await async_client.tracking.with_raw_response.verify(
- "",
+ await async_client.tenants.tracking.with_raw_response.verify(
+ tracking_id="",
+ tenant_id="cm6abc123def456",
)
diff --git a/tests/api_resources/tenants/test_usage.py b/tests/api_resources/tenants/test_usage.py
new file mode 100644
index 0000000..edb2820
--- /dev/null
+++ b/tests/api_resources/tenants/test_usage.py
@@ -0,0 +1,217 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from ark import Ark, AsyncArk
+from tests.utils import assert_matches_type
+from ark.types.tenants import (
+ UsageRetrieveResponse,
+ UsageRetrieveTimeseriesResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestUsage:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ usage = client.tenants.usage.retrieve(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Ark) -> None:
+ usage = client.tenants.usage.retrieve(
+ tenant_id="cm6abc123def456",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.tenants.usage.with_raw_response.retrieve(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.tenants.usage.with_streaming_response.retrieve(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.usage.with_raw_response.retrieve(
+ tenant_id="",
+ )
+
+ @parametrize
+ def test_method_retrieve_timeseries(self, client: Ark) -> None:
+ usage = client.tenants.usage.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_method_retrieve_timeseries_with_all_params(self, client: Ark) -> None:
+ usage = client.tenants.usage.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ granularity="hour",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_timeseries(self, client: Ark) -> None:
+ response = client.tenants.usage.with_raw_response.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_timeseries(self, client: Ark) -> None:
+ with client.tenants.usage.with_streaming_response.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = response.parse()
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_timeseries(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.usage.with_raw_response.retrieve_timeseries(
+ tenant_id="",
+ )
+
+
+class TestAsyncUsage:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ usage = await async_client.tenants.usage.retrieve(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.tenants.usage.retrieve(
+ tenant_id="cm6abc123def456",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.usage.with_raw_response.retrieve(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.usage.with_streaming_response.retrieve(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.usage.with_raw_response.retrieve(
+ tenant_id="",
+ )
+
+ @parametrize
+ async def test_method_retrieve_timeseries(self, async_client: AsyncArk) -> None:
+ usage = await async_client.tenants.usage.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_method_retrieve_timeseries_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.tenants.usage.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ granularity="hour",
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_timeseries(self, async_client: AsyncArk) -> None:
+ response = await async_client.tenants.usage.with_raw_response.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_timeseries(self, async_client: AsyncArk) -> None:
+ async with async_client.tenants.usage.with_streaming_response.retrieve_timeseries(
+ tenant_id="cm6abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ usage = await response.parse()
+ assert_matches_type(UsageRetrieveTimeseriesResponse, usage, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_timeseries(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.usage.with_raw_response.retrieve_timeseries(
+ tenant_id="",
+ )
diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/tenants/test_webhooks.py
similarity index 60%
rename from tests/api_resources/test_webhooks.py
rename to tests/api_resources/tenants/test_webhooks.py
index e994815..8a5284f 100644
--- a/tests/api_resources/test_webhooks.py
+++ b/tests/api_resources/tenants/test_webhooks.py
@@ -8,7 +8,8 @@
import pytest
from ark import Ark, AsyncArk
-from ark.types import (
+from tests.utils import assert_matches_type
+from ark.types.tenants import (
WebhookListResponse,
WebhookTestResponse,
WebhookCreateResponse,
@@ -19,7 +20,6 @@
WebhookReplayDeliveryResponse,
WebhookRetrieveDeliveryResponse,
)
-from tests.utils import assert_matches_type
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -29,7 +29,8 @@ class TestWebhooks:
@parametrize
def test_method_create(self, client: Ark) -> None:
- webhook = client.webhooks.create(
+ webhook = client.tenants.webhooks.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
)
@@ -37,7 +38,8 @@ def test_method_create(self, client: Ark) -> None:
@parametrize
def test_method_create_with_all_params(self, client: Ark) -> None:
- webhook = client.webhooks.create(
+ webhook = client.tenants.webhooks.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
all_events=True,
@@ -48,7 +50,8 @@ def test_method_create_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_create(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.create(
+ response = client.tenants.webhooks.with_raw_response.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
)
@@ -60,7 +63,8 @@ def test_raw_response_create(self, client: Ark) -> None:
@parametrize
def test_streaming_response_create(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.create(
+ with client.tenants.webhooks.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
) as response:
@@ -72,17 +76,28 @@ def test_streaming_response_create(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_create(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.create(
+ tenant_id="",
+ name="My App Webhook",
+ url="https://myapp.com/webhooks/email",
+ )
+
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- webhook = client.webhooks.retrieve(
- "webhookId",
+ webhook = client.tenants.webhooks.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.retrieve(
- "webhookId",
+ response = client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -92,8 +107,9 @@ def test_raw_response_retrieve(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.retrieve(
- "webhookId",
+ with client.tenants.webhooks.with_streaming_response.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -105,22 +121,31 @@ 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 `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.retrieve(
- "",
+ client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_update(self, client: Ark) -> None:
- webhook = client.webhooks.update(
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
@parametrize
def test_method_update_with_all_params(self, client: Ark) -> None:
- webhook = client.webhooks.update(
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
all_events=True,
enabled=True,
events=["string"],
@@ -131,8 +156,9 @@ def test_method_update_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_update(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.update(
- webhook_id="webhookId",
+ response = client.tenants.webhooks.with_raw_response.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -142,8 +168,9 @@ def test_raw_response_update(self, client: Ark) -> None:
@parametrize
def test_streaming_response_update(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.update(
- webhook_id="webhookId",
+ with client.tenants.webhooks.with_streaming_response.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -155,19 +182,30 @@ def test_streaming_response_update(self, client: Ark) -> None:
@parametrize
def test_path_params_update(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.update(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.update(
+ client.tenants.webhooks.with_raw_response.update(
webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_list(self, client: Ark) -> None:
- webhook = client.webhooks.list()
+ webhook = client.tenants.webhooks.list(
+ "cm6abc123def456",
+ )
assert_matches_type(WebhookListResponse, webhook, path=["response"])
@parametrize
def test_raw_response_list(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.list()
+ response = client.tenants.webhooks.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -176,7 +214,9 @@ def test_raw_response_list(self, client: Ark) -> None:
@parametrize
def test_streaming_response_list(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.list() as response:
+ with client.tenants.webhooks.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -185,17 +225,26 @@ def test_streaming_response_list(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_path_params_list(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.list(
+ "",
+ )
+
@parametrize
def test_method_delete(self, client: Ark) -> None:
- webhook = client.webhooks.delete(
- "webhookId",
+ webhook = client.tenants.webhooks.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
@parametrize
def test_raw_response_delete(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.delete(
- "webhookId",
+ response = client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -205,8 +254,9 @@ def test_raw_response_delete(self, client: Ark) -> None:
@parametrize
def test_streaming_response_delete(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.delete(
- "webhookId",
+ with client.tenants.webhooks.with_streaming_response.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -218,22 +268,31 @@ def test_streaming_response_delete(self, client: Ark) -> None:
@parametrize
def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.delete(
- "",
+ client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_list_deliveries(self, client: Ark) -> None:
- webhook = client.webhooks.list_deliveries(
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookListDeliveriesResponse, webhook, path=["response"])
@parametrize
def test_method_list_deliveries_with_all_params(self, client: Ark) -> None:
- webhook = client.webhooks.list_deliveries(
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
after=0,
before=0,
event="MessageSent",
@@ -245,8 +304,9 @@ def test_method_list_deliveries_with_all_params(self, client: Ark) -> None:
@parametrize
def test_raw_response_list_deliveries(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.list_deliveries(
- webhook_id="webhookId",
+ response = client.tenants.webhooks.with_raw_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -256,8 +316,9 @@ def test_raw_response_list_deliveries(self, client: Ark) -> None:
@parametrize
def test_streaming_response_list_deliveries(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.list_deliveries(
- webhook_id="webhookId",
+ with client.tenants.webhooks.with_streaming_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -269,24 +330,33 @@ def test_streaming_response_list_deliveries(self, client: Ark) -> None:
@parametrize
def test_path_params_list_deliveries(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.list_deliveries(
+ client.tenants.webhooks.with_raw_response.list_deliveries(
webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
def test_method_replay_delivery(self, client: Ark) -> None:
- webhook = client.webhooks.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
@parametrize
def test_raw_response_replay_delivery(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ response = client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert response.is_closed is True
@@ -296,9 +366,10 @@ def test_raw_response_replay_delivery(self, client: Ark) -> None:
@parametrize
def test_streaming_response_replay_delivery(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ with client.tenants.webhooks.with_streaming_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -310,31 +381,42 @@ def test_streaming_response_replay_delivery(self, client: Ark) -> None:
@parametrize
def test_path_params_replay_delivery(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="",
+ webhook_id="123",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.replay_delivery(
- delivery_id="deliveryId",
+ client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
webhook_id="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
- client.webhooks.with_raw_response.replay_delivery(
+ client.tenants.webhooks.with_raw_response.replay_delivery(
delivery_id="",
- webhook_id="webhookId",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
@parametrize
def test_method_retrieve_delivery(self, client: Ark) -> None:
- webhook = client.webhooks.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
@parametrize
def test_raw_response_retrieve_delivery(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ response = client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert response.is_closed is True
@@ -344,9 +426,10 @@ def test_raw_response_retrieve_delivery(self, client: Ark) -> None:
@parametrize
def test_streaming_response_retrieve_delivery(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ with client.tenants.webhooks.with_streaming_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -358,30 +441,41 @@ def test_streaming_response_retrieve_delivery(self, client: Ark) -> None:
@parametrize
def test_path_params_retrieve_delivery(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="",
+ webhook_id="123",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.retrieve_delivery(
- delivery_id="deliveryId",
+ client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
webhook_id="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
- client.webhooks.with_raw_response.retrieve_delivery(
+ client.tenants.webhooks.with_raw_response.retrieve_delivery(
delivery_id="",
- webhook_id="webhookId",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
@parametrize
def test_method_test(self, client: Ark) -> None:
- webhook = client.webhooks.test(
- webhook_id="webhookId",
+ webhook = client.tenants.webhooks.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
assert_matches_type(WebhookTestResponse, webhook, path=["response"])
@parametrize
def test_raw_response_test(self, client: Ark) -> None:
- response = client.webhooks.with_raw_response.test(
- webhook_id="webhookId",
+ response = client.tenants.webhooks.with_raw_response.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
@@ -392,8 +486,9 @@ def test_raw_response_test(self, client: Ark) -> None:
@parametrize
def test_streaming_response_test(self, client: Ark) -> None:
- with client.webhooks.with_streaming_response.test(
- webhook_id="webhookId",
+ with client.tenants.webhooks.with_streaming_response.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
) as response:
assert not response.is_closed
@@ -406,9 +501,17 @@ def test_streaming_response_test(self, client: Ark) -> None:
@parametrize
def test_path_params_test(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ client.tenants.webhooks.with_raw_response.test(
+ webhook_id="123",
+ tenant_id="",
+ event="MessageSent",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- client.webhooks.with_raw_response.test(
+ client.tenants.webhooks.with_raw_response.test(
webhook_id="",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
@@ -420,7 +523,8 @@ class TestAsyncWebhooks:
@parametrize
async def test_method_create(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.create(
+ webhook = await async_client.tenants.webhooks.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
)
@@ -428,7 +532,8 @@ async def test_method_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.create(
+ webhook = await async_client.tenants.webhooks.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
all_events=True,
@@ -439,7 +544,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> No
@parametrize
async def test_raw_response_create(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.create(
+ response = await async_client.tenants.webhooks.with_raw_response.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
)
@@ -451,7 +557,8 @@ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.create(
+ async with async_client.tenants.webhooks.with_streaming_response.create(
+ tenant_id="cm6abc123def456",
name="My App Webhook",
url="https://myapp.com/webhooks/email",
) as response:
@@ -463,17 +570,28 @@ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.create(
+ tenant_id="",
+ name="My App Webhook",
+ url="https://myapp.com/webhooks/email",
+ )
+
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.retrieve(
- "webhookId",
+ webhook = await async_client.tenants.webhooks.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.retrieve(
- "webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -483,8 +601,9 @@ 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.webhooks.with_streaming_response.retrieve(
- "webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.retrieve(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -496,22 +615,31 @@ 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 `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.retrieve(
- "",
+ await async_client.tenants.webhooks.with_raw_response.retrieve(
+ webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_update(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.update(
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
@parametrize
async def test_method_update_with_all_params(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.update(
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
all_events=True,
enabled=True,
events=["string"],
@@ -522,8 +650,9 @@ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> No
@parametrize
async def test_raw_response_update(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.update(
- webhook_id="webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -533,8 +662,9 @@ async def test_raw_response_update(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.update(
- webhook_id="webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.update(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -546,19 +676,30 @@ async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_update(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.update(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.update(
+ await async_client.tenants.webhooks.with_raw_response.update(
webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_list(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.list()
+ webhook = await async_client.tenants.webhooks.list(
+ "cm6abc123def456",
+ )
assert_matches_type(WebhookListResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.list()
+ response = await async_client.tenants.webhooks.with_raw_response.list(
+ "cm6abc123def456",
+ )
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -567,7 +708,9 @@ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.list() as response:
+ async with async_client.tenants.webhooks.with_streaming_response.list(
+ "cm6abc123def456",
+ ) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -576,17 +719,26 @@ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.list(
+ "",
+ )
+
@parametrize
async def test_method_delete(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.delete(
- "webhookId",
+ webhook = await async_client.tenants.webhooks.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.delete(
- "webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -596,8 +748,9 @@ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.delete(
- "webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.delete(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -609,22 +762,31 @@ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.delete(
- "",
+ await async_client.tenants.webhooks.with_raw_response.delete(
+ webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_list_deliveries(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.list_deliveries(
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert_matches_type(WebhookListDeliveriesResponse, webhook, path=["response"])
@parametrize
async def test_method_list_deliveries_with_all_params(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.list_deliveries(
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
after=0,
before=0,
event="MessageSent",
@@ -636,8 +798,9 @@ async def test_method_list_deliveries_with_all_params(self, async_client: AsyncA
@parametrize
async def test_raw_response_list_deliveries(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.list_deliveries(
- webhook_id="webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
)
assert response.is_closed is True
@@ -647,8 +810,9 @@ async def test_raw_response_list_deliveries(self, async_client: AsyncArk) -> Non
@parametrize
async def test_streaming_response_list_deliveries(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.list_deliveries(
- webhook_id="webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -660,24 +824,33 @@ async def test_streaming_response_list_deliveries(self, async_client: AsyncArk)
@parametrize
async def test_path_params_list_deliveries(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.list_deliveries(
+ webhook_id="123",
+ tenant_id="",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.list_deliveries(
+ await async_client.tenants.webhooks.with_raw_response.list_deliveries(
webhook_id="",
+ tenant_id="cm6abc123def456",
)
@parametrize
async def test_method_replay_delivery(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_replay_delivery(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert response.is_closed is True
@@ -687,9 +860,10 @@ async def test_raw_response_replay_delivery(self, async_client: AsyncArk) -> Non
@parametrize
async def test_streaming_response_replay_delivery(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.replay_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -701,31 +875,42 @@ async def test_streaming_response_replay_delivery(self, async_client: AsyncArk)
@parametrize
async def test_path_params_replay_delivery(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="",
+ webhook_id="123",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.replay_delivery(
- delivery_id="deliveryId",
+ await async_client.tenants.webhooks.with_raw_response.replay_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
webhook_id="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
- await async_client.webhooks.with_raw_response.replay_delivery(
+ await async_client.tenants.webhooks.with_raw_response.replay_delivery(
delivery_id="",
- webhook_id="webhookId",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
@parametrize
async def test_method_retrieve_delivery(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_retrieve_delivery(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
assert response.is_closed is True
@@ -735,9 +920,10 @@ async def test_raw_response_retrieve_delivery(self, async_client: AsyncArk) -> N
@parametrize
async def test_streaming_response_retrieve_delivery(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.retrieve_delivery(
- delivery_id="deliveryId",
- webhook_id="webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -749,30 +935,41 @@ async def test_streaming_response_retrieve_delivery(self, async_client: AsyncArk
@parametrize
async def test_path_params_retrieve_delivery(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="",
+ webhook_id="123",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.retrieve_delivery(
- delivery_id="deliveryId",
+ await async_client.tenants.webhooks.with_raw_response.retrieve_delivery(
+ delivery_id="whr_abc123def456",
+ tenant_id="cm6abc123def456",
webhook_id="",
)
with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
- await async_client.webhooks.with_raw_response.retrieve_delivery(
+ await async_client.tenants.webhooks.with_raw_response.retrieve_delivery(
delivery_id="",
- webhook_id="webhookId",
+ tenant_id="cm6abc123def456",
+ webhook_id="123",
)
@parametrize
async def test_method_test(self, async_client: AsyncArk) -> None:
- webhook = await async_client.webhooks.test(
- webhook_id="webhookId",
+ webhook = await async_client.tenants.webhooks.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
assert_matches_type(WebhookTestResponse, webhook, path=["response"])
@parametrize
async def test_raw_response_test(self, async_client: AsyncArk) -> None:
- response = await async_client.webhooks.with_raw_response.test(
- webhook_id="webhookId",
+ response = await async_client.tenants.webhooks.with_raw_response.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
@@ -783,8 +980,9 @@ async def test_raw_response_test(self, async_client: AsyncArk) -> None:
@parametrize
async def test_streaming_response_test(self, async_client: AsyncArk) -> None:
- async with async_client.webhooks.with_streaming_response.test(
- webhook_id="webhookId",
+ async with async_client.tenants.webhooks.with_streaming_response.test(
+ webhook_id="123",
+ tenant_id="cm6abc123def456",
event="MessageSent",
) as response:
assert not response.is_closed
@@ -797,8 +995,16 @@ async def test_streaming_response_test(self, async_client: AsyncArk) -> None:
@parametrize
async def test_path_params_test(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
+ await async_client.tenants.webhooks.with_raw_response.test(
+ webhook_id="123",
+ tenant_id="",
+ event="MessageSent",
+ )
+
with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
- await async_client.webhooks.with_raw_response.test(
+ await async_client.tenants.webhooks.with_raw_response.test(
webhook_id="",
+ tenant_id="cm6abc123def456",
event="MessageSent",
)
diff --git a/tests/api_resources/test_usage.py b/tests/api_resources/test_usage.py
index a98b600..69d4699 100644
--- a/tests/api_resources/test_usage.py
+++ b/tests/api_resources/test_usage.py
@@ -9,16 +9,12 @@
from ark import Ark, AsyncArk
from ark.types import (
+ OrgUsageSummary,
+ TenantUsageItem,
UsageExportResponse,
- UsageRetrieveResponse,
- UsageRetrieveTenantUsageResponse,
- UsageRetrieveTenantTimeseriesResponse,
)
from tests.utils import assert_matches_type
-from ark.pagination import SyncOffsetPagination, AsyncOffsetPagination
-from ark.types.bulk_tenant_usage import Tenant
-
-# pyright: reportDeprecated=false
+from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -28,30 +24,34 @@ class TestUsage:
@parametrize
def test_method_retrieve(self, client: Ark) -> None:
- with pytest.warns(DeprecationWarning):
- usage = client.usage.retrieve()
+ usage = client.usage.retrieve()
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.retrieve(
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
@parametrize
def test_raw_response_retrieve(self, client: Ark) -> None:
- with pytest.warns(DeprecationWarning):
- response = client.usage.with_raw_response.retrieve()
+ response = client.usage.with_raw_response.retrieve()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
@parametrize
def test_streaming_response_retrieve(self, client: Ark) -> None:
- with pytest.warns(DeprecationWarning):
- with client.usage.with_streaming_response.retrieve() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ with client.usage.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ usage = response.parse()
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -92,138 +92,43 @@ def test_streaming_response_export(self, client: Ark) -> None:
assert cast(Any, response.is_closed) is True
@parametrize
- def test_method_list_by_tenant(self, client: Ark) -> None:
- usage = client.usage.list_by_tenant()
- assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
+ def test_method_list_tenants(self, client: Ark) -> None:
+ usage = client.usage.list_tenants()
+ assert_matches_type(SyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- def test_method_list_by_tenant_with_all_params(self, client: Ark) -> None:
- usage = client.usage.list_by_tenant(
- limit=1,
+ def test_method_list_tenants_with_all_params(self, client: Ark) -> None:
+ usage = client.usage.list_tenants(
min_sent=0,
- offset=0,
+ page=1,
period="period",
+ per_page=1,
sort="sent",
status="active",
timezone="timezone",
)
- assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
-
- @parametrize
- def test_raw_response_list_by_tenant(self, client: Ark) -> None:
- response = client.usage.with_raw_response.list_by_tenant()
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = response.parse()
- assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
-
- @parametrize
- def test_streaming_response_list_by_tenant(self, client: Ark) -> None:
- with client.usage.with_streaming_response.list_by_tenant() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- usage = response.parse()
- assert_matches_type(SyncOffsetPagination[Tenant], usage, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_method_retrieve_tenant_timeseries(self, client: Ark) -> None:
- usage = client.usage.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- )
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
-
- @parametrize
- def test_method_retrieve_tenant_timeseries_with_all_params(self, client: Ark) -> None:
- usage = client.usage.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- granularity="hour",
- period="period",
- timezone="timezone",
- )
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+ assert_matches_type(SyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- def test_raw_response_retrieve_tenant_timeseries(self, client: Ark) -> None:
- response = client.usage.with_raw_response.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- )
+ def test_raw_response_list_tenants(self, client: Ark) -> None:
+ response = client.usage.with_raw_response.list_tenants()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = response.parse()
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+ assert_matches_type(SyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- def test_streaming_response_retrieve_tenant_timeseries(self, client: Ark) -> None:
- with client.usage.with_streaming_response.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- ) as response:
+ def test_streaming_response_list_tenants(self, client: Ark) -> None:
+ with client.usage.with_streaming_response.list_tenants() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = response.parse()
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
+ assert_matches_type(SyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
assert cast(Any, response.is_closed) is True
- @parametrize
- def test_path_params_retrieve_tenant_timeseries(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
- client.usage.with_raw_response.retrieve_tenant_timeseries(
- tenant_id="",
- )
-
- @parametrize
- def test_method_retrieve_tenant_usage(self, client: Ark) -> None:
- usage = client.usage.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- )
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- def test_method_retrieve_tenant_usage_with_all_params(self, client: Ark) -> None:
- usage = client.usage.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- period="period",
- timezone="timezone",
- )
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- def test_raw_response_retrieve_tenant_usage(self, client: Ark) -> None:
- response = client.usage.with_raw_response.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = response.parse()
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- def test_streaming_response_retrieve_tenant_usage(self, client: Ark) -> None:
- with client.usage.with_streaming_response.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- usage = response.parse()
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_retrieve_tenant_usage(self, client: Ark) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
- client.usage.with_raw_response.retrieve_tenant_usage(
- tenant_id="",
- )
-
class TestAsyncUsage:
parametrize = pytest.mark.parametrize(
@@ -232,30 +137,34 @@ class TestAsyncUsage:
@parametrize
async def test_method_retrieve(self, async_client: AsyncArk) -> None:
- with pytest.warns(DeprecationWarning):
- usage = await async_client.usage.retrieve()
+ usage = await async_client.usage.retrieve()
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.retrieve(
+ period="period",
+ timezone="timezone",
+ )
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
- with pytest.warns(DeprecationWarning):
- response = await async_client.usage.with_raw_response.retrieve()
+ response = await async_client.usage.with_raw_response.retrieve()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = await response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
- with pytest.warns(DeprecationWarning):
- async with async_client.usage.with_streaming_response.retrieve() as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ async with async_client.usage.with_streaming_response.retrieve() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = await response.parse()
- assert_matches_type(UsageRetrieveResponse, usage, path=["response"])
+ usage = await response.parse()
+ assert_matches_type(OrgUsageSummary, usage, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -296,134 +205,39 @@ async def test_streaming_response_export(self, async_client: AsyncArk) -> None:
assert cast(Any, response.is_closed) is True
@parametrize
- async def test_method_list_by_tenant(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.list_by_tenant()
- assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+ async def test_method_list_tenants(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.list_tenants()
+ assert_matches_type(AsyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- async def test_method_list_by_tenant_with_all_params(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.list_by_tenant(
- limit=1,
+ async def test_method_list_tenants_with_all_params(self, async_client: AsyncArk) -> None:
+ usage = await async_client.usage.list_tenants(
min_sent=0,
- offset=0,
+ page=1,
period="period",
+ per_page=1,
sort="sent",
status="active",
timezone="timezone",
)
- assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+ assert_matches_type(AsyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- async def test_raw_response_list_by_tenant(self, async_client: AsyncArk) -> None:
- response = await async_client.usage.with_raw_response.list_by_tenant()
+ async def test_raw_response_list_tenants(self, async_client: AsyncArk) -> None:
+ response = await async_client.usage.with_raw_response.list_tenants()
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = await response.parse()
- assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+ assert_matches_type(AsyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
@parametrize
- async def test_streaming_response_list_by_tenant(self, async_client: AsyncArk) -> None:
- async with async_client.usage.with_streaming_response.list_by_tenant() as response:
+ async def test_streaming_response_list_tenants(self, async_client: AsyncArk) -> None:
+ async with async_client.usage.with_streaming_response.list_tenants() as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
usage = await response.parse()
- assert_matches_type(AsyncOffsetPagination[Tenant], usage, path=["response"])
+ assert_matches_type(AsyncPageNumberPagination[TenantUsageItem], usage, path=["response"])
assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_method_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- )
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
-
- @parametrize
- async def test_method_retrieve_tenant_timeseries_with_all_params(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- granularity="hour",
- period="period",
- timezone="timezone",
- )
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
-
- @parametrize
- async def test_raw_response_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
- response = await async_client.usage.with_raw_response.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = await response.parse()
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
-
- @parametrize
- async def test_streaming_response_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
- async with async_client.usage.with_streaming_response.retrieve_tenant_timeseries(
- tenant_id="cm6abc123def456",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- usage = await response.parse()
- assert_matches_type(UsageRetrieveTenantTimeseriesResponse, usage, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_retrieve_tenant_timeseries(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
- await async_client.usage.with_raw_response.retrieve_tenant_timeseries(
- tenant_id="",
- )
-
- @parametrize
- async def test_method_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- )
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- async def test_method_retrieve_tenant_usage_with_all_params(self, async_client: AsyncArk) -> None:
- usage = await async_client.usage.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- period="period",
- timezone="timezone",
- )
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- async def test_raw_response_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
- response = await async_client.usage.with_raw_response.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- usage = await response.parse()
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- @parametrize
- async def test_streaming_response_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
- async with async_client.usage.with_streaming_response.retrieve_tenant_usage(
- tenant_id="cm6abc123def456",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- usage = await response.parse()
- assert_matches_type(UsageRetrieveTenantUsageResponse, usage, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_retrieve_tenant_usage(self, async_client: AsyncArk) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `tenant_id` but received ''"):
- await async_client.usage.with_raw_response.retrieve_tenant_usage(
- tenant_id="",
- )
From f0a53a79606bece5fd5cf9b2dad7a580b8bae94e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 20:33:33 +0000
Subject: [PATCH 5/6] feat(api): add Platform webhooks
---
.stats.yml | 8 +-
api.md | 32 +
src/ark/_client.py | 39 +-
src/ark/resources/__init__.py | 14 +
src/ark/resources/platform/__init__.py | 33 +
src/ark/resources/platform/platform.py | 102 ++
src/ark/resources/platform/webhooks.py | 1140 +++++++++++++++++
src/ark/types/platform/__init__.py | 17 +
.../types/platform/webhook_create_params.py | 30 +
.../types/platform/webhook_create_response.py | 52 +
.../types/platform/webhook_delete_response.py | 20 +
.../webhook_list_deliveries_params.py | 44 +
.../webhook_list_deliveries_response.py | 54 +
.../types/platform/webhook_list_response.py | 35 +
.../webhook_replay_delivery_response.py | 42 +
.../webhook_retrieve_delivery_response.py | 81 ++
.../platform/webhook_retrieve_response.py | 52 +
src/ark/types/platform/webhook_test_params.py | 23 +
.../types/platform/webhook_test_response.py | 33 +
.../types/platform/webhook_update_params.py | 33 +
.../types/platform/webhook_update_response.py | 52 +
tests/api_resources/platform/__init__.py | 1 +
tests/api_resources/platform/test_webhooks.py | 735 +++++++++++
23 files changed, 2667 insertions(+), 5 deletions(-)
create mode 100644 src/ark/resources/platform/__init__.py
create mode 100644 src/ark/resources/platform/platform.py
create mode 100644 src/ark/resources/platform/webhooks.py
create mode 100644 src/ark/types/platform/__init__.py
create mode 100644 src/ark/types/platform/webhook_create_params.py
create mode 100644 src/ark/types/platform/webhook_create_response.py
create mode 100644 src/ark/types/platform/webhook_delete_response.py
create mode 100644 src/ark/types/platform/webhook_list_deliveries_params.py
create mode 100644 src/ark/types/platform/webhook_list_deliveries_response.py
create mode 100644 src/ark/types/platform/webhook_list_response.py
create mode 100644 src/ark/types/platform/webhook_replay_delivery_response.py
create mode 100644 src/ark/types/platform/webhook_retrieve_delivery_response.py
create mode 100644 src/ark/types/platform/webhook_retrieve_response.py
create mode 100644 src/ark/types/platform/webhook_test_params.py
create mode 100644 src/ark/types/platform/webhook_test_response.py
create mode 100644 src/ark/types/platform/webhook_update_params.py
create mode 100644 src/ark/types/platform/webhook_update_response.py
create mode 100644 tests/api_resources/platform/__init__.py
create mode 100644 tests/api_resources/platform/test_webhooks.py
diff --git a/.stats.yml b/.stats.yml
index 8c774e8..f1de74b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 49
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-37bce0c9a4d738ca0eed70eb9c19cd4528a79eec733c2b4366217fb2c31abd78.yml
-openapi_spec_hash: 7dbd1f812cf95dfa8ba631ced146e255
-config_hash: 1c0067ab4449e3fb20b923cd64edc829
+configured_endpoints: 58
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-ee4b9d190e3aaa146b08bc0ffed1c802dc353c3fdc37fc0097f2350ab3714b70.yml
+openapi_spec_hash: 0dad8b2e562ba7ce879425ab92169d85
+config_hash: b70b11b10fc614f91f1c6f028b40780f
diff --git a/api.md b/api.md
index 052f4ed..65acd82 100644
--- a/api.md
+++ b/api.md
@@ -235,3 +235,35 @@ Methods:
- client.tenants.usage.retrieve(tenant_id, \*\*params) -> UsageRetrieveResponse
- client.tenants.usage.retrieve_timeseries(tenant_id, \*\*params) -> UsageRetrieveTimeseriesResponse
+
+# Platform
+
+## Webhooks
+
+Types:
+
+```python
+from ark.types.platform import (
+ WebhookCreateResponse,
+ WebhookRetrieveResponse,
+ WebhookUpdateResponse,
+ WebhookListResponse,
+ WebhookDeleteResponse,
+ WebhookListDeliveriesResponse,
+ WebhookReplayDeliveryResponse,
+ WebhookRetrieveDeliveryResponse,
+ WebhookTestResponse,
+)
+```
+
+Methods:
+
+- client.platform.webhooks.create(\*\*params) -> WebhookCreateResponse
+- client.platform.webhooks.retrieve(webhook_id) -> WebhookRetrieveResponse
+- client.platform.webhooks.update(webhook_id, \*\*params) -> WebhookUpdateResponse
+- client.platform.webhooks.list() -> WebhookListResponse
+- client.platform.webhooks.delete(webhook_id) -> WebhookDeleteResponse
+- client.platform.webhooks.list_deliveries(\*\*params) -> SyncPageNumberPagination[WebhookListDeliveriesResponse]
+- client.platform.webhooks.replay_delivery(delivery_id) -> WebhookReplayDeliveryResponse
+- client.platform.webhooks.retrieve_delivery(delivery_id) -> WebhookRetrieveDeliveryResponse
+- client.platform.webhooks.test(webhook_id, \*\*params) -> WebhookTestResponse
diff --git a/src/ark/_client.py b/src/ark/_client.py
index d9d5633..40de787 100644
--- a/src/ark/_client.py
+++ b/src/ark/_client.py
@@ -31,12 +31,13 @@
)
if TYPE_CHECKING:
- from .resources import logs, usage, emails, limits, tenants
+ from .resources import logs, usage, emails, limits, tenants, platform
from .resources.logs import LogsResource, AsyncLogsResource
from .resources.usage import UsageResource, AsyncUsageResource
from .resources.emails import EmailsResource, AsyncEmailsResource
from .resources.limits import LimitsResource, AsyncLimitsResource
from .resources.tenants.tenants import TenantsResource, AsyncTenantsResource
+ from .resources.platform.platform import PlatformResource, AsyncPlatformResource
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Ark", "AsyncArk", "Client", "AsyncClient"]
@@ -126,6 +127,12 @@ def tenants(self) -> TenantsResource:
return TenantsResource(self)
+ @cached_property
+ def platform(self) -> PlatformResource:
+ from .resources.platform import PlatformResource
+
+ return PlatformResource(self)
+
@cached_property
def with_raw_response(self) -> ArkWithRawResponse:
return ArkWithRawResponse(self)
@@ -324,6 +331,12 @@ def tenants(self) -> AsyncTenantsResource:
return AsyncTenantsResource(self)
+ @cached_property
+ def platform(self) -> AsyncPlatformResource:
+ from .resources.platform import AsyncPlatformResource
+
+ return AsyncPlatformResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncArkWithRawResponse:
return AsyncArkWithRawResponse(self)
@@ -473,6 +486,12 @@ def tenants(self) -> tenants.TenantsResourceWithRawResponse:
return TenantsResourceWithRawResponse(self._client.tenants)
+ @cached_property
+ def platform(self) -> platform.PlatformResourceWithRawResponse:
+ from .resources.platform import PlatformResourceWithRawResponse
+
+ return PlatformResourceWithRawResponse(self._client.platform)
+
class AsyncArkWithRawResponse:
_client: AsyncArk
@@ -510,6 +529,12 @@ def tenants(self) -> tenants.AsyncTenantsResourceWithRawResponse:
return AsyncTenantsResourceWithRawResponse(self._client.tenants)
+ @cached_property
+ def platform(self) -> platform.AsyncPlatformResourceWithRawResponse:
+ from .resources.platform import AsyncPlatformResourceWithRawResponse
+
+ return AsyncPlatformResourceWithRawResponse(self._client.platform)
+
class ArkWithStreamedResponse:
_client: Ark
@@ -547,6 +572,12 @@ def tenants(self) -> tenants.TenantsResourceWithStreamingResponse:
return TenantsResourceWithStreamingResponse(self._client.tenants)
+ @cached_property
+ def platform(self) -> platform.PlatformResourceWithStreamingResponse:
+ from .resources.platform import PlatformResourceWithStreamingResponse
+
+ return PlatformResourceWithStreamingResponse(self._client.platform)
+
class AsyncArkWithStreamedResponse:
_client: AsyncArk
@@ -584,6 +615,12 @@ def tenants(self) -> tenants.AsyncTenantsResourceWithStreamingResponse:
return AsyncTenantsResourceWithStreamingResponse(self._client.tenants)
+ @cached_property
+ def platform(self) -> platform.AsyncPlatformResourceWithStreamingResponse:
+ from .resources.platform import AsyncPlatformResourceWithStreamingResponse
+
+ return AsyncPlatformResourceWithStreamingResponse(self._client.platform)
+
Client = Ark
diff --git a/src/ark/resources/__init__.py b/src/ark/resources/__init__.py
index a98e05b..9908f11 100644
--- a/src/ark/resources/__init__.py
+++ b/src/ark/resources/__init__.py
@@ -40,6 +40,14 @@
TenantsResourceWithStreamingResponse,
AsyncTenantsResourceWithStreamingResponse,
)
+from .platform import (
+ PlatformResource,
+ AsyncPlatformResource,
+ PlatformResourceWithRawResponse,
+ AsyncPlatformResourceWithRawResponse,
+ PlatformResourceWithStreamingResponse,
+ AsyncPlatformResourceWithStreamingResponse,
+)
__all__ = [
"EmailsResource",
@@ -72,4 +80,10 @@
"AsyncTenantsResourceWithRawResponse",
"TenantsResourceWithStreamingResponse",
"AsyncTenantsResourceWithStreamingResponse",
+ "PlatformResource",
+ "AsyncPlatformResource",
+ "PlatformResourceWithRawResponse",
+ "AsyncPlatformResourceWithRawResponse",
+ "PlatformResourceWithStreamingResponse",
+ "AsyncPlatformResourceWithStreamingResponse",
]
diff --git a/src/ark/resources/platform/__init__.py b/src/ark/resources/platform/__init__.py
new file mode 100644
index 0000000..17e4578
--- /dev/null
+++ b/src/ark/resources/platform/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .platform import (
+ PlatformResource,
+ AsyncPlatformResource,
+ PlatformResourceWithRawResponse,
+ AsyncPlatformResourceWithRawResponse,
+ PlatformResourceWithStreamingResponse,
+ AsyncPlatformResourceWithStreamingResponse,
+)
+from .webhooks import (
+ WebhooksResource,
+ AsyncWebhooksResource,
+ WebhooksResourceWithRawResponse,
+ AsyncWebhooksResourceWithRawResponse,
+ WebhooksResourceWithStreamingResponse,
+ AsyncWebhooksResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "WebhooksResource",
+ "AsyncWebhooksResource",
+ "WebhooksResourceWithRawResponse",
+ "AsyncWebhooksResourceWithRawResponse",
+ "WebhooksResourceWithStreamingResponse",
+ "AsyncWebhooksResourceWithStreamingResponse",
+ "PlatformResource",
+ "AsyncPlatformResource",
+ "PlatformResourceWithRawResponse",
+ "AsyncPlatformResourceWithRawResponse",
+ "PlatformResourceWithStreamingResponse",
+ "AsyncPlatformResourceWithStreamingResponse",
+]
diff --git a/src/ark/resources/platform/platform.py b/src/ark/resources/platform/platform.py
new file mode 100644
index 0000000..5d6e945
--- /dev/null
+++ b/src/ark/resources/platform/platform.py
@@ -0,0 +1,102 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .webhooks import (
+ WebhooksResource,
+ AsyncWebhooksResource,
+ WebhooksResourceWithRawResponse,
+ AsyncWebhooksResourceWithRawResponse,
+ WebhooksResourceWithStreamingResponse,
+ AsyncWebhooksResourceWithStreamingResponse,
+)
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+
+__all__ = ["PlatformResource", "AsyncPlatformResource"]
+
+
+class PlatformResource(SyncAPIResource):
+ @cached_property
+ def webhooks(self) -> WebhooksResource:
+ return WebhooksResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> PlatformResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return PlatformResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> PlatformResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return PlatformResourceWithStreamingResponse(self)
+
+
+class AsyncPlatformResource(AsyncAPIResource):
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResource:
+ return AsyncWebhooksResource(self._client)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncPlatformResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncPlatformResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncPlatformResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncPlatformResourceWithStreamingResponse(self)
+
+
+class PlatformResourceWithRawResponse:
+ def __init__(self, platform: PlatformResource) -> None:
+ self._platform = platform
+
+ @cached_property
+ def webhooks(self) -> WebhooksResourceWithRawResponse:
+ return WebhooksResourceWithRawResponse(self._platform.webhooks)
+
+
+class AsyncPlatformResourceWithRawResponse:
+ def __init__(self, platform: AsyncPlatformResource) -> None:
+ self._platform = platform
+
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResourceWithRawResponse:
+ return AsyncWebhooksResourceWithRawResponse(self._platform.webhooks)
+
+
+class PlatformResourceWithStreamingResponse:
+ def __init__(self, platform: PlatformResource) -> None:
+ self._platform = platform
+
+ @cached_property
+ def webhooks(self) -> WebhooksResourceWithStreamingResponse:
+ return WebhooksResourceWithStreamingResponse(self._platform.webhooks)
+
+
+class AsyncPlatformResourceWithStreamingResponse:
+ def __init__(self, platform: AsyncPlatformResource) -> None:
+ self._platform = platform
+
+ @cached_property
+ def webhooks(self) -> AsyncWebhooksResourceWithStreamingResponse:
+ return AsyncWebhooksResourceWithStreamingResponse(self._platform.webhooks)
diff --git a/src/ark/resources/platform/webhooks.py b/src/ark/resources/platform/webhooks.py
new file mode 100644
index 0000000..7a30429
--- /dev/null
+++ b/src/ark/resources/platform/webhooks.py
@@ -0,0 +1,1140 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from ..._utils import maybe_transform, async_maybe_transform
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ..._base_client import AsyncPaginator, make_request_options
+from ...types.platform import (
+ webhook_test_params,
+ webhook_create_params,
+ webhook_update_params,
+ webhook_list_deliveries_params,
+)
+from ...types.platform.webhook_list_response import WebhookListResponse
+from ...types.platform.webhook_test_response import WebhookTestResponse
+from ...types.platform.webhook_create_response import WebhookCreateResponse
+from ...types.platform.webhook_delete_response import WebhookDeleteResponse
+from ...types.platform.webhook_update_response import WebhookUpdateResponse
+from ...types.platform.webhook_retrieve_response import WebhookRetrieveResponse
+from ...types.platform.webhook_list_deliveries_response import WebhookListDeliveriesResponse
+from ...types.platform.webhook_replay_delivery_response import WebhookReplayDeliveryResponse
+from ...types.platform.webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse
+
+__all__ = ["WebhooksResource", "AsyncWebhooksResource"]
+
+
+class WebhooksResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> WebhooksResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return WebhooksResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return WebhooksResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ name: str,
+ url: str,
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebhookCreateResponse:
+ """
+ Create a platform webhook to receive email event notifications from all tenants.
+
+ Platform webhooks receive events from **all tenants** in your organization. Each
+ webhook payload includes a `tenant_id` field to identify which tenant the event
+ belongs to.
+
+ **Available events:**
+
+ - `MessageSent` - Email accepted by recipient server
+ - `MessageDeliveryFailed` - Delivery permanently failed
+ - `MessageDelayed` - Delivery temporarily failed, will retry
+ - `MessageBounced` - Email bounced
+ - `MessageHeld` - Email held for review
+ - `MessageLinkClicked` - Recipient clicked a link
+ - `MessageLoaded` - Recipient opened the email
+ - `DomainDNSError` - Domain DNS issue detected
+
+ **Webhook payload includes:**
+
+ - `event` - The event type
+ - `tenant_id` - The tenant that sent the email
+ - `timestamp` - Unix timestamp of the event
+ - `payload` - Event-specific data (message details, status, etc.)
+
+ Args:
+ name: Display name for the webhook
+
+ url: Webhook endpoint URL (must be HTTPS)
+
+ events: Events to subscribe to. Empty array means all events.
+
+ 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
+ """
+ return self._post(
+ "/platform/webhooks",
+ body=maybe_transform(
+ {
+ "name": name,
+ "url": url,
+ "events": events,
+ },
+ webhook_create_params.WebhookCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookCreateResponse,
+ )
+
+ def retrieve(
+ self,
+ webhook_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,
+ ) -> WebhookRetrieveResponse:
+ """
+ Get detailed information about a specific platform webhook.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return self._get(
+ f"/platform/webhooks/{webhook_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookRetrieveResponse,
+ )
+
+ def update(
+ self,
+ webhook_id: str,
+ *,
+ enabled: bool | Omit = omit,
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ | Omit = omit,
+ name: str | Omit = omit,
+ url: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebhookUpdateResponse:
+ """
+ Update a platform webhook's configuration.
+
+ You can update:
+
+ - `name` - Display name for the webhook
+ - `url` - The endpoint URL (must be HTTPS)
+ - `events` - Array of event types to receive (empty array = all events)
+ - `enabled` - Enable or disable the webhook
+
+ Args:
+ enabled: Enable or disable the webhook
+
+ events: Events to subscribe to. Empty array means all events.
+
+ name: Display name for the webhook
+
+ url: Webhook endpoint URL (must be HTTPS)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return self._patch(
+ f"/platform/webhooks/{webhook_id}",
+ body=maybe_transform(
+ {
+ "enabled": enabled,
+ "events": events,
+ "name": name,
+ "url": url,
+ },
+ webhook_update_params.WebhookUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookUpdateResponse,
+ )
+
+ def list(
+ self,
+ *,
+ # 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,
+ ) -> WebhookListResponse:
+ """
+ Get all platform webhook endpoints configured for your organization.
+
+ Platform webhooks receive events from **all tenants** in your organization,
+ unlike tenant webhooks which only receive events for a specific tenant. This is
+ useful for centralized event processing and monitoring.
+ """
+ return self._get(
+ "/platform/webhooks",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookListResponse,
+ )
+
+ def delete(
+ self,
+ webhook_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,
+ ) -> WebhookDeleteResponse:
+ """Delete a platform webhook.
+
+ This stops all event delivery to the webhook URL.
+ This action cannot be undone.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return self._delete(
+ f"/platform/webhooks/{webhook_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookDeleteResponse,
+ )
+
+ def list_deliveries(
+ self,
+ *,
+ after: int | Omit = omit,
+ before: int | Omit = omit,
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ | Omit = omit,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ success: bool | Omit = omit,
+ tenant_id: str | Omit = omit,
+ webhook_id: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> SyncPageNumberPagination[WebhookListDeliveriesResponse]:
+ """
+ Get a paginated list of platform webhook delivery attempts.
+
+ Filter by:
+
+ - `webhookId` - Specific webhook
+ - `tenantId` - Specific tenant
+ - `event` - Specific event type
+ - `success` - Successful (2xx) or failed deliveries
+ - `before`/`after` - Time range (Unix timestamps)
+
+ Deliveries are returned in reverse chronological order.
+
+ Args:
+ after: Only deliveries after this Unix timestamp
+
+ before: Only deliveries before this Unix timestamp
+
+ event: Filter by event type
+
+ page: Page number (default 1)
+
+ per_page: Items per page (default 30, max 100)
+
+ success: Filter by delivery success
+
+ tenant_id: Filter by tenant ID
+
+ webhook_id: Filter by platform webhook ID
+
+ 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
+ """
+ return self._get_api_list(
+ "/platform/webhooks/deliveries",
+ page=SyncPageNumberPagination[WebhookListDeliveriesResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "after": after,
+ "before": before,
+ "event": event,
+ "page": page,
+ "per_page": per_page,
+ "success": success,
+ "tenant_id": tenant_id,
+ "webhook_id": webhook_id,
+ },
+ webhook_list_deliveries_params.WebhookListDeliveriesParams,
+ ),
+ ),
+ model=WebhookListDeliveriesResponse,
+ )
+
+ def replay_delivery(
+ self,
+ delivery_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,
+ ) -> WebhookReplayDeliveryResponse:
+ """
+ Replay a previous platform webhook delivery.
+
+ This re-sends the original payload with a new timestamp and delivery ID. Useful
+ for recovering from temporary endpoint failures.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not delivery_id:
+ raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
+ return self._post(
+ f"/platform/webhooks/deliveries/{delivery_id}/replay",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookReplayDeliveryResponse,
+ )
+
+ def retrieve_delivery(
+ self,
+ delivery_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,
+ ) -> WebhookRetrieveDeliveryResponse:
+ """
+ Get detailed information about a specific platform webhook delivery.
+
+ Returns the complete request payload, headers, response, and timing info.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not delivery_id:
+ raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
+ return self._get(
+ f"/platform/webhooks/deliveries/{delivery_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookRetrieveDeliveryResponse,
+ )
+
+ def test(
+ self,
+ webhook_id: str,
+ *,
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ],
+ # 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,
+ ) -> WebhookTestResponse:
+ """
+ Send a test payload to your platform webhook endpoint.
+
+ Use this to:
+
+ - Verify your webhook URL is accessible
+ - Test your payload handling code
+ - Ensure your server responds correctly
+
+ The test payload is marked with `_test: true` so you can distinguish test events
+ from real events.
+
+ Args:
+ event: Event type to simulate
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return self._post(
+ f"/platform/webhooks/{webhook_id}/test",
+ body=maybe_transform({"event": event}, webhook_test_params.WebhookTestParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookTestResponse,
+ )
+
+
+class AsyncWebhooksResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncWebhooksResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncWebhooksResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/ArkHQ-io/ark-python#with_streaming_response
+ """
+ return AsyncWebhooksResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ name: str,
+ url: str,
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ | 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebhookCreateResponse:
+ """
+ Create a platform webhook to receive email event notifications from all tenants.
+
+ Platform webhooks receive events from **all tenants** in your organization. Each
+ webhook payload includes a `tenant_id` field to identify which tenant the event
+ belongs to.
+
+ **Available events:**
+
+ - `MessageSent` - Email accepted by recipient server
+ - `MessageDeliveryFailed` - Delivery permanently failed
+ - `MessageDelayed` - Delivery temporarily failed, will retry
+ - `MessageBounced` - Email bounced
+ - `MessageHeld` - Email held for review
+ - `MessageLinkClicked` - Recipient clicked a link
+ - `MessageLoaded` - Recipient opened the email
+ - `DomainDNSError` - Domain DNS issue detected
+
+ **Webhook payload includes:**
+
+ - `event` - The event type
+ - `tenant_id` - The tenant that sent the email
+ - `timestamp` - Unix timestamp of the event
+ - `payload` - Event-specific data (message details, status, etc.)
+
+ Args:
+ name: Display name for the webhook
+
+ url: Webhook endpoint URL (must be HTTPS)
+
+ events: Events to subscribe to. Empty array means all events.
+
+ 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
+ """
+ return await self._post(
+ "/platform/webhooks",
+ body=await async_maybe_transform(
+ {
+ "name": name,
+ "url": url,
+ "events": events,
+ },
+ webhook_create_params.WebhookCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookCreateResponse,
+ )
+
+ async def retrieve(
+ self,
+ webhook_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,
+ ) -> WebhookRetrieveResponse:
+ """
+ Get detailed information about a specific platform webhook.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return await self._get(
+ f"/platform/webhooks/{webhook_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookRetrieveResponse,
+ )
+
+ async def update(
+ self,
+ webhook_id: str,
+ *,
+ enabled: bool | Omit = omit,
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ | Omit = omit,
+ name: str | Omit = omit,
+ url: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebhookUpdateResponse:
+ """
+ Update a platform webhook's configuration.
+
+ You can update:
+
+ - `name` - Display name for the webhook
+ - `url` - The endpoint URL (must be HTTPS)
+ - `events` - Array of event types to receive (empty array = all events)
+ - `enabled` - Enable or disable the webhook
+
+ Args:
+ enabled: Enable or disable the webhook
+
+ events: Events to subscribe to. Empty array means all events.
+
+ name: Display name for the webhook
+
+ url: Webhook endpoint URL (must be HTTPS)
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return await self._patch(
+ f"/platform/webhooks/{webhook_id}",
+ body=await async_maybe_transform(
+ {
+ "enabled": enabled,
+ "events": events,
+ "name": name,
+ "url": url,
+ },
+ webhook_update_params.WebhookUpdateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookUpdateResponse,
+ )
+
+ async def list(
+ self,
+ *,
+ # 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,
+ ) -> WebhookListResponse:
+ """
+ Get all platform webhook endpoints configured for your organization.
+
+ Platform webhooks receive events from **all tenants** in your organization,
+ unlike tenant webhooks which only receive events for a specific tenant. This is
+ useful for centralized event processing and monitoring.
+ """
+ return await self._get(
+ "/platform/webhooks",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookListResponse,
+ )
+
+ async def delete(
+ self,
+ webhook_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,
+ ) -> WebhookDeleteResponse:
+ """Delete a platform webhook.
+
+ This stops all event delivery to the webhook URL.
+ This action cannot be undone.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return await self._delete(
+ f"/platform/webhooks/{webhook_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookDeleteResponse,
+ )
+
+ def list_deliveries(
+ self,
+ *,
+ after: int | Omit = omit,
+ before: int | Omit = omit,
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ | Omit = omit,
+ page: int | Omit = omit,
+ per_page: int | Omit = omit,
+ success: bool | Omit = omit,
+ tenant_id: str | Omit = omit,
+ webhook_id: 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,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncPaginator[WebhookListDeliveriesResponse, AsyncPageNumberPagination[WebhookListDeliveriesResponse]]:
+ """
+ Get a paginated list of platform webhook delivery attempts.
+
+ Filter by:
+
+ - `webhookId` - Specific webhook
+ - `tenantId` - Specific tenant
+ - `event` - Specific event type
+ - `success` - Successful (2xx) or failed deliveries
+ - `before`/`after` - Time range (Unix timestamps)
+
+ Deliveries are returned in reverse chronological order.
+
+ Args:
+ after: Only deliveries after this Unix timestamp
+
+ before: Only deliveries before this Unix timestamp
+
+ event: Filter by event type
+
+ page: Page number (default 1)
+
+ per_page: Items per page (default 30, max 100)
+
+ success: Filter by delivery success
+
+ tenant_id: Filter by tenant ID
+
+ webhook_id: Filter by platform webhook ID
+
+ 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
+ """
+ return self._get_api_list(
+ "/platform/webhooks/deliveries",
+ page=AsyncPageNumberPagination[WebhookListDeliveriesResponse],
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform(
+ {
+ "after": after,
+ "before": before,
+ "event": event,
+ "page": page,
+ "per_page": per_page,
+ "success": success,
+ "tenant_id": tenant_id,
+ "webhook_id": webhook_id,
+ },
+ webhook_list_deliveries_params.WebhookListDeliveriesParams,
+ ),
+ ),
+ model=WebhookListDeliveriesResponse,
+ )
+
+ async def replay_delivery(
+ self,
+ delivery_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,
+ ) -> WebhookReplayDeliveryResponse:
+ """
+ Replay a previous platform webhook delivery.
+
+ This re-sends the original payload with a new timestamp and delivery ID. Useful
+ for recovering from temporary endpoint failures.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not delivery_id:
+ raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
+ return await self._post(
+ f"/platform/webhooks/deliveries/{delivery_id}/replay",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookReplayDeliveryResponse,
+ )
+
+ async def retrieve_delivery(
+ self,
+ delivery_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,
+ ) -> WebhookRetrieveDeliveryResponse:
+ """
+ Get detailed information about a specific platform webhook delivery.
+
+ Returns the complete request payload, headers, response, and timing info.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not delivery_id:
+ raise ValueError(f"Expected a non-empty value for `delivery_id` but received {delivery_id!r}")
+ return await self._get(
+ f"/platform/webhooks/deliveries/{delivery_id}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookRetrieveDeliveryResponse,
+ )
+
+ async def test(
+ self,
+ webhook_id: str,
+ *,
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ],
+ # 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,
+ ) -> WebhookTestResponse:
+ """
+ Send a test payload to your platform webhook endpoint.
+
+ Use this to:
+
+ - Verify your webhook URL is accessible
+ - Test your payload handling code
+ - Ensure your server responds correctly
+
+ The test payload is marked with `_test: true` so you can distinguish test events
+ from real events.
+
+ Args:
+ event: Event type to simulate
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not webhook_id:
+ raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}")
+ return await self._post(
+ f"/platform/webhooks/{webhook_id}/test",
+ body=await async_maybe_transform({"event": event}, webhook_test_params.WebhookTestParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebhookTestResponse,
+ )
+
+
+class WebhooksResourceWithRawResponse:
+ def __init__(self, webhooks: WebhooksResource) -> None:
+ self._webhooks = webhooks
+
+ self.create = to_raw_response_wrapper(
+ webhooks.create,
+ )
+ self.retrieve = to_raw_response_wrapper(
+ webhooks.retrieve,
+ )
+ self.update = to_raw_response_wrapper(
+ webhooks.update,
+ )
+ self.list = to_raw_response_wrapper(
+ webhooks.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ webhooks.delete,
+ )
+ self.list_deliveries = to_raw_response_wrapper(
+ webhooks.list_deliveries,
+ )
+ self.replay_delivery = to_raw_response_wrapper(
+ webhooks.replay_delivery,
+ )
+ self.retrieve_delivery = to_raw_response_wrapper(
+ webhooks.retrieve_delivery,
+ )
+ self.test = to_raw_response_wrapper(
+ webhooks.test,
+ )
+
+
+class AsyncWebhooksResourceWithRawResponse:
+ def __init__(self, webhooks: AsyncWebhooksResource) -> None:
+ self._webhooks = webhooks
+
+ self.create = async_to_raw_response_wrapper(
+ webhooks.create,
+ )
+ self.retrieve = async_to_raw_response_wrapper(
+ webhooks.retrieve,
+ )
+ self.update = async_to_raw_response_wrapper(
+ webhooks.update,
+ )
+ self.list = async_to_raw_response_wrapper(
+ webhooks.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ webhooks.delete,
+ )
+ self.list_deliveries = async_to_raw_response_wrapper(
+ webhooks.list_deliveries,
+ )
+ self.replay_delivery = async_to_raw_response_wrapper(
+ webhooks.replay_delivery,
+ )
+ self.retrieve_delivery = async_to_raw_response_wrapper(
+ webhooks.retrieve_delivery,
+ )
+ self.test = async_to_raw_response_wrapper(
+ webhooks.test,
+ )
+
+
+class WebhooksResourceWithStreamingResponse:
+ def __init__(self, webhooks: WebhooksResource) -> None:
+ self._webhooks = webhooks
+
+ self.create = to_streamed_response_wrapper(
+ webhooks.create,
+ )
+ self.retrieve = to_streamed_response_wrapper(
+ webhooks.retrieve,
+ )
+ self.update = to_streamed_response_wrapper(
+ webhooks.update,
+ )
+ self.list = to_streamed_response_wrapper(
+ webhooks.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ webhooks.delete,
+ )
+ self.list_deliveries = to_streamed_response_wrapper(
+ webhooks.list_deliveries,
+ )
+ self.replay_delivery = to_streamed_response_wrapper(
+ webhooks.replay_delivery,
+ )
+ self.retrieve_delivery = to_streamed_response_wrapper(
+ webhooks.retrieve_delivery,
+ )
+ self.test = to_streamed_response_wrapper(
+ webhooks.test,
+ )
+
+
+class AsyncWebhooksResourceWithStreamingResponse:
+ def __init__(self, webhooks: AsyncWebhooksResource) -> None:
+ self._webhooks = webhooks
+
+ self.create = async_to_streamed_response_wrapper(
+ webhooks.create,
+ )
+ self.retrieve = async_to_streamed_response_wrapper(
+ webhooks.retrieve,
+ )
+ self.update = async_to_streamed_response_wrapper(
+ webhooks.update,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ webhooks.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ webhooks.delete,
+ )
+ self.list_deliveries = async_to_streamed_response_wrapper(
+ webhooks.list_deliveries,
+ )
+ self.replay_delivery = async_to_streamed_response_wrapper(
+ webhooks.replay_delivery,
+ )
+ self.retrieve_delivery = async_to_streamed_response_wrapper(
+ webhooks.retrieve_delivery,
+ )
+ self.test = async_to_streamed_response_wrapper(
+ webhooks.test,
+ )
diff --git a/src/ark/types/platform/__init__.py b/src/ark/types/platform/__init__.py
new file mode 100644
index 0000000..6e70e72
--- /dev/null
+++ b/src/ark/types/platform/__init__.py
@@ -0,0 +1,17 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .webhook_test_params import WebhookTestParams as WebhookTestParams
+from .webhook_create_params import WebhookCreateParams as WebhookCreateParams
+from .webhook_list_response import WebhookListResponse as WebhookListResponse
+from .webhook_test_response import WebhookTestResponse as WebhookTestResponse
+from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams
+from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse
+from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse
+from .webhook_update_response import WebhookUpdateResponse as WebhookUpdateResponse
+from .webhook_retrieve_response import WebhookRetrieveResponse as WebhookRetrieveResponse
+from .webhook_list_deliveries_params import WebhookListDeliveriesParams as WebhookListDeliveriesParams
+from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse
+from .webhook_replay_delivery_response import WebhookReplayDeliveryResponse as WebhookReplayDeliveryResponse
+from .webhook_retrieve_delivery_response import WebhookRetrieveDeliveryResponse as WebhookRetrieveDeliveryResponse
diff --git a/src/ark/types/platform/webhook_create_params.py b/src/ark/types/platform/webhook_create_params.py
new file mode 100644
index 0000000..574222f
--- /dev/null
+++ b/src/ark/types/platform/webhook_create_params.py
@@ -0,0 +1,30 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["WebhookCreateParams"]
+
+
+class WebhookCreateParams(TypedDict, total=False):
+ name: Required[str]
+ """Display name for the webhook"""
+
+ url: Required[str]
+ """Webhook endpoint URL (must be HTTPS)"""
+
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Events to subscribe to. Empty array means all events."""
diff --git a/src/ark/types/platform/webhook_create_response.py b/src/ark/types/platform/webhook_create_response.py
new file mode 100644
index 0000000..37ca2ca
--- /dev/null
+++ b/src/ark/types/platform/webhook_create_response.py
@@ -0,0 +1,52 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookCreateResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: str
+ """Platform webhook ID"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+
+ enabled: bool
+ """Whether the webhook is active"""
+
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Subscribed events (empty = all events)"""
+
+ name: str
+ """Webhook name for identification"""
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+
+ url: str
+ """Webhook endpoint URL"""
+
+
+class WebhookCreateResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_delete_response.py b/src/ark/types/platform/webhook_delete_response.py
new file mode 100644
index 0000000..aee22ba
--- /dev/null
+++ b/src/ark/types/platform/webhook_delete_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookDeleteResponse", "Data"]
+
+
+class Data(BaseModel):
+ message: str
+
+
+class WebhookDeleteResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_list_deliveries_params.py b/src/ark/types/platform/webhook_list_deliveries_params.py
new file mode 100644
index 0000000..3d00ef0
--- /dev/null
+++ b/src/ark/types/platform/webhook_list_deliveries_params.py
@@ -0,0 +1,44 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Annotated, TypedDict
+
+from ..._utils import PropertyInfo
+
+__all__ = ["WebhookListDeliveriesParams"]
+
+
+class WebhookListDeliveriesParams(TypedDict, total=False):
+ after: int
+ """Only deliveries after this Unix timestamp"""
+
+ before: int
+ """Only deliveries before this Unix timestamp"""
+
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ """Filter by event type"""
+
+ page: int
+ """Page number (default 1)"""
+
+ per_page: Annotated[int, PropertyInfo(alias="perPage")]
+ """Items per page (default 30, max 100)"""
+
+ success: bool
+ """Filter by delivery success"""
+
+ tenant_id: Annotated[str, PropertyInfo(alias="tenantId")]
+ """Filter by tenant ID"""
+
+ webhook_id: Annotated[str, PropertyInfo(alias="webhookId")]
+ """Filter by platform webhook ID"""
diff --git a/src/ark/types/platform/webhook_list_deliveries_response.py b/src/ark/types/platform/webhook_list_deliveries_response.py
new file mode 100644
index 0000000..25160c7
--- /dev/null
+++ b/src/ark/types/platform/webhook_list_deliveries_response.py
@@ -0,0 +1,54 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+
+__all__ = ["WebhookListDeliveriesResponse"]
+
+
+class WebhookListDeliveriesResponse(BaseModel):
+ """Summary of a platform webhook delivery attempt"""
+
+ id: str
+ """Unique delivery ID"""
+
+ attempt: int
+ """Attempt number (1 for first attempt, higher for retries)"""
+
+ event: Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ """Event type"""
+
+ status_code: Optional[int] = FieldInfo(alias="statusCode", default=None)
+ """HTTP status code returned by your endpoint (null if connection failed)"""
+
+ success: bool
+ """Whether delivery was successful (2xx response)"""
+
+ tenant_id: str = FieldInfo(alias="tenantId")
+ """Tenant that triggered the event"""
+
+ timestamp: datetime
+ """When the delivery was attempted"""
+
+ url: str
+ """Endpoint URL the delivery was sent to"""
+
+ webhook_id: str = FieldInfo(alias="webhookId")
+ """Platform webhook ID"""
+
+ will_retry: bool = FieldInfo(alias="willRetry")
+ """Whether this delivery will be retried"""
diff --git a/src/ark/types/platform/webhook_list_response.py b/src/ark/types/platform/webhook_list_response.py
new file mode 100644
index 0000000..3afef57
--- /dev/null
+++ b/src/ark/types/platform/webhook_list_response.py
@@ -0,0 +1,35 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookListResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: str
+ """Platform webhook ID"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+
+ enabled: bool
+
+ events: List[str]
+
+ name: str
+
+ url: str
+
+
+class WebhookListResponse(BaseModel):
+ data: List[Data]
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_replay_delivery_response.py b/src/ark/types/platform/webhook_replay_delivery_response.py
new file mode 100644
index 0000000..311bef0
--- /dev/null
+++ b/src/ark/types/platform/webhook_replay_delivery_response.py
@@ -0,0 +1,42 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookReplayDeliveryResponse", "Data"]
+
+
+class Data(BaseModel):
+ duration: int
+ """Request duration in milliseconds"""
+
+ new_delivery_id: str = FieldInfo(alias="newDeliveryId")
+ """ID of the new delivery created by the replay"""
+
+ original_delivery_id: str = FieldInfo(alias="originalDeliveryId")
+ """ID of the original delivery that was replayed"""
+
+ status_code: Optional[int] = FieldInfo(alias="statusCode", default=None)
+ """HTTP status code from your endpoint"""
+
+ success: bool
+ """Whether the replay was successful"""
+
+ timestamp: datetime
+ """When the replay was executed"""
+
+
+class WebhookReplayDeliveryResponse(BaseModel):
+ """Result of replaying a platform webhook delivery"""
+
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_retrieve_delivery_response.py b/src/ark/types/platform/webhook_retrieve_delivery_response.py
new file mode 100644
index 0000000..6da891a
--- /dev/null
+++ b/src/ark/types/platform/webhook_retrieve_delivery_response.py
@@ -0,0 +1,81 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Dict, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookRetrieveDeliveryResponse", "Data", "DataRequest", "DataResponse"]
+
+
+class DataRequest(BaseModel):
+ """Request details"""
+
+ headers: Optional[Dict[str, str]] = None
+ """Request headers including signature"""
+
+ payload: Optional[Dict[str, object]] = None
+ """The complete webhook payload that was sent"""
+
+
+class DataResponse(BaseModel):
+ """Response details"""
+
+ body: Optional[str] = None
+ """Response body (truncated if too large)"""
+
+ duration: Optional[int] = None
+ """Response time in milliseconds"""
+
+
+class Data(BaseModel):
+ id: str
+ """Unique delivery ID"""
+
+ attempt: int
+ """Attempt number"""
+
+ event: str
+ """Event type"""
+
+ request: DataRequest
+ """Request details"""
+
+ response: DataResponse
+ """Response details"""
+
+ status_code: Optional[int] = FieldInfo(alias="statusCode", default=None)
+ """HTTP status code from your endpoint"""
+
+ success: bool
+ """Whether delivery was successful"""
+
+ tenant_id: str = FieldInfo(alias="tenantId")
+ """Tenant that triggered the event"""
+
+ timestamp: datetime
+ """When delivery was attempted"""
+
+ url: str
+ """Endpoint URL"""
+
+ webhook_id: str = FieldInfo(alias="webhookId")
+ """Platform webhook ID"""
+
+ webhook_name: str = FieldInfo(alias="webhookName")
+ """Platform webhook name"""
+
+ will_retry: bool = FieldInfo(alias="willRetry")
+ """Whether this will be retried"""
+
+
+class WebhookRetrieveDeliveryResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_retrieve_response.py b/src/ark/types/platform/webhook_retrieve_response.py
new file mode 100644
index 0000000..b589458
--- /dev/null
+++ b/src/ark/types/platform/webhook_retrieve_response.py
@@ -0,0 +1,52 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookRetrieveResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: str
+ """Platform webhook ID"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+
+ enabled: bool
+ """Whether the webhook is active"""
+
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Subscribed events (empty = all events)"""
+
+ name: str
+ """Webhook name for identification"""
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+
+ url: str
+ """Webhook endpoint URL"""
+
+
+class WebhookRetrieveResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_test_params.py b/src/ark/types/platform/webhook_test_params.py
new file mode 100644
index 0000000..3f91fca
--- /dev/null
+++ b/src/ark/types/platform/webhook_test_params.py
@@ -0,0 +1,23 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["WebhookTestParams"]
+
+
+class WebhookTestParams(TypedDict, total=False):
+ event: Required[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Event type to simulate"""
diff --git a/src/ark/types/platform/webhook_test_response.py b/src/ark/types/platform/webhook_test_response.py
new file mode 100644
index 0000000..56036e1
--- /dev/null
+++ b/src/ark/types/platform/webhook_test_response.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import Optional
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookTestResponse", "Data"]
+
+
+class Data(BaseModel):
+ duration_ms: int = FieldInfo(alias="durationMs")
+ """Request duration in milliseconds"""
+
+ status_code: int = FieldInfo(alias="statusCode")
+ """HTTP status code from the webhook endpoint"""
+
+ success: bool
+ """Whether the webhook endpoint responded with a 2xx status"""
+
+ error: Optional[str] = None
+ """Error message if the request failed"""
+
+
+class WebhookTestResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/src/ark/types/platform/webhook_update_params.py b/src/ark/types/platform/webhook_update_params.py
new file mode 100644
index 0000000..6da78ab
--- /dev/null
+++ b/src/ark/types/platform/webhook_update_params.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["WebhookUpdateParams"]
+
+
+class WebhookUpdateParams(TypedDict, total=False):
+ enabled: bool
+ """Enable or disable the webhook"""
+
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Events to subscribe to. Empty array means all events."""
+
+ name: str
+ """Display name for the webhook"""
+
+ url: str
+ """Webhook endpoint URL (must be HTTPS)"""
diff --git a/src/ark/types/platform/webhook_update_response.py b/src/ark/types/platform/webhook_update_response.py
new file mode 100644
index 0000000..d300235
--- /dev/null
+++ b/src/ark/types/platform/webhook_update_response.py
@@ -0,0 +1,52 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from ..._models import BaseModel
+from ..shared.api_meta import APIMeta
+
+__all__ = ["WebhookUpdateResponse", "Data"]
+
+
+class Data(BaseModel):
+ id: str
+ """Platform webhook ID"""
+
+ created_at: datetime = FieldInfo(alias="createdAt")
+
+ enabled: bool
+ """Whether the webhook is active"""
+
+ events: List[
+ Literal[
+ "MessageSent",
+ "MessageDelayed",
+ "MessageDeliveryFailed",
+ "MessageHeld",
+ "MessageBounced",
+ "MessageLinkClicked",
+ "MessageLoaded",
+ "DomainDNSError",
+ ]
+ ]
+ """Subscribed events (empty = all events)"""
+
+ name: str
+ """Webhook name for identification"""
+
+ updated_at: datetime = FieldInfo(alias="updatedAt")
+
+ url: str
+ """Webhook endpoint URL"""
+
+
+class WebhookUpdateResponse(BaseModel):
+ data: Data
+
+ meta: APIMeta
+
+ success: Literal[True]
diff --git a/tests/api_resources/platform/__init__.py b/tests/api_resources/platform/__init__.py
new file mode 100644
index 0000000..fd8019a
--- /dev/null
+++ b/tests/api_resources/platform/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/platform/test_webhooks.py b/tests/api_resources/platform/test_webhooks.py
new file mode 100644
index 0000000..b64b0a6
--- /dev/null
+++ b/tests/api_resources/platform/test_webhooks.py
@@ -0,0 +1,735 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from ark import Ark, AsyncArk
+from tests.utils import assert_matches_type
+from ark.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
+from ark.types.platform import (
+ WebhookListResponse,
+ WebhookTestResponse,
+ WebhookCreateResponse,
+ WebhookDeleteResponse,
+ WebhookUpdateResponse,
+ WebhookRetrieveResponse,
+ WebhookListDeliveriesResponse,
+ WebhookReplayDeliveryResponse,
+ WebhookRetrieveDeliveryResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestWebhooks:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ )
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ events=["MessageSent", "MessageDeliveryFailed", "MessageBounced"],
+ )
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_retrieve(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.retrieve(
+ "pwh_abc123def456",
+ )
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.retrieve(
+ "pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.retrieve(
+ "pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ client.platform.webhooks.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ def test_method_update(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.update(
+ webhook_id="pwh_abc123def456",
+ )
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_method_update_with_all_params(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.update(
+ webhook_id="pwh_abc123def456",
+ enabled=True,
+ events=["MessageSent"],
+ name="x",
+ url="https://example.com",
+ )
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_update(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.update(
+ webhook_id="pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_update(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.update(
+ webhook_id="pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_update(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ client.platform.webhooks.with_raw_response.update(
+ webhook_id="",
+ )
+
+ @parametrize
+ def test_method_list(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.list()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_delete(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.delete(
+ "pwh_abc123def456",
+ )
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.delete(
+ "pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.delete(
+ "pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ client.platform.webhooks.with_raw_response.delete(
+ "",
+ )
+
+ @parametrize
+ def test_method_list_deliveries(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.list_deliveries()
+ assert_matches_type(SyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ def test_method_list_deliveries_with_all_params(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.list_deliveries(
+ after=0,
+ before=0,
+ event="MessageSent",
+ page=0,
+ per_page=100,
+ success=True,
+ tenant_id="tenantId",
+ webhook_id="webhookId",
+ )
+ assert_matches_type(SyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_list_deliveries(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.list_deliveries()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(SyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list_deliveries(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.list_deliveries() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(SyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_replay_delivery(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.replay_delivery(
+ "pwd_abc123def456",
+ )
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_replay_delivery(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.replay_delivery(
+ "pwd_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_replay_delivery(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.replay_delivery(
+ "pwd_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_replay_delivery(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
+ client.platform.webhooks.with_raw_response.replay_delivery(
+ "",
+ )
+
+ @parametrize
+ def test_method_retrieve_delivery(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.retrieve_delivery(
+ "pwd_abc123def456",
+ )
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_retrieve_delivery(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.retrieve_delivery(
+ "pwd_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_retrieve_delivery(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.retrieve_delivery(
+ "pwd_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_retrieve_delivery(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
+ client.platform.webhooks.with_raw_response.retrieve_delivery(
+ "",
+ )
+
+ @parametrize
+ def test_method_test(self, client: Ark) -> None:
+ webhook = client.platform.webhooks.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ )
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_raw_response_test(self, client: Ark) -> None:
+ response = client.platform.webhooks.with_raw_response.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = response.parse()
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ @parametrize
+ def test_streaming_response_test(self, client: Ark) -> None:
+ with client.platform.webhooks.with_streaming_response.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = response.parse()
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_test(self, client: Ark) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ client.platform.webhooks.with_raw_response.test(
+ webhook_id="",
+ event="MessageSent",
+ )
+
+
+class TestAsyncWebhooks:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ )
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ events=["MessageSent", "MessageDeliveryFailed", "MessageBounced"],
+ )
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.create(
+ name="Central Event Processor",
+ url="https://myplatform.com/webhooks/email-events",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookCreateResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_retrieve(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.retrieve(
+ "pwh_abc123def456",
+ )
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.retrieve(
+ "pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.retrieve(
+ "pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.retrieve(
+ "",
+ )
+
+ @parametrize
+ async def test_method_update(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.update(
+ webhook_id="pwh_abc123def456",
+ )
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_method_update_with_all_params(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.update(
+ webhook_id="pwh_abc123def456",
+ enabled=True,
+ events=["MessageSent"],
+ name="x",
+ url="https://example.com",
+ )
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_update(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.update(
+ webhook_id="pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_update(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.update(
+ webhook_id="pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookUpdateResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_update(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.update(
+ webhook_id="",
+ )
+
+ @parametrize
+ async def test_method_list(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.list()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookListResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.delete(
+ "pwh_abc123def456",
+ )
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.delete(
+ "pwh_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.delete(
+ "pwh_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookDeleteResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.delete(
+ "",
+ )
+
+ @parametrize
+ async def test_method_list_deliveries(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.list_deliveries()
+ assert_matches_type(AsyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ async def test_method_list_deliveries_with_all_params(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.list_deliveries(
+ after=0,
+ before=0,
+ event="MessageSent",
+ page=0,
+ per_page=100,
+ success=True,
+ tenant_id="tenantId",
+ webhook_id="webhookId",
+ )
+ assert_matches_type(AsyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list_deliveries(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.list_deliveries()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list_deliveries(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.list_deliveries() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(AsyncPageNumberPagination[WebhookListDeliveriesResponse], webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_replay_delivery(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.replay_delivery(
+ "pwd_abc123def456",
+ )
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_replay_delivery(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.replay_delivery(
+ "pwd_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_replay_delivery(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.replay_delivery(
+ "pwd_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookReplayDeliveryResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_replay_delivery(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.replay_delivery(
+ "",
+ )
+
+ @parametrize
+ async def test_method_retrieve_delivery(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.retrieve_delivery(
+ "pwd_abc123def456",
+ )
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_retrieve_delivery(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.retrieve_delivery(
+ "pwd_abc123def456",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_retrieve_delivery(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.retrieve_delivery(
+ "pwd_abc123def456",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookRetrieveDeliveryResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_retrieve_delivery(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `delivery_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.retrieve_delivery(
+ "",
+ )
+
+ @parametrize
+ async def test_method_test(self, async_client: AsyncArk) -> None:
+ webhook = await async_client.platform.webhooks.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ )
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_raw_response_test(self, async_client: AsyncArk) -> None:
+ response = await async_client.platform.webhooks.with_raw_response.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ webhook = await response.parse()
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_test(self, async_client: AsyncArk) -> None:
+ async with async_client.platform.webhooks.with_streaming_response.test(
+ webhook_id="pwh_abc123def456",
+ event="MessageSent",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ webhook = await response.parse()
+ assert_matches_type(WebhookTestResponse, webhook, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_test(self, async_client: AsyncArk) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"):
+ await async_client.platform.webhooks.with_raw_response.test(
+ webhook_id="",
+ event="MessageSent",
+ )
From 5a4c3a3a94c6ce818f3bbe1a8b7b94771e8eff71 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 20:33:52 +0000
Subject: [PATCH 6/6] release: 0.18.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 12 ++++++++++++
pyproject.toml | 2 +-
src/ark/_version.py | 2 +-
4 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6db19b9..4ad3fef 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.17.0"
+ ".": "0.18.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a605d7..b1e8e50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## 0.18.0 (2026-02-05)
+
+Full Changelog: [v0.17.0...v0.18.0](https://github.com/ArkHQ-io/ark-python/compare/v0.17.0...v0.18.0)
+
+### Features
+
+* **api:** add Credentials endpoint ([0ff55ca](https://github.com/ArkHQ-io/ark-python/commit/0ff55caabeb38ad0046cf489c28f8dad3caddfc2))
+* **api:** add Platform webhooks ([f0a53a7](https://github.com/ArkHQ-io/ark-python/commit/f0a53a79606bece5fd5cf9b2dad7a580b8bae94e))
+* **api:** endpoint updates ([0e54a04](https://github.com/ArkHQ-io/ark-python/commit/0e54a042195b1134b7c5cba9ee2ca4b98f35b361))
+* **api:** standardization improvements ([b52928d](https://github.com/ArkHQ-io/ark-python/commit/b52928d6147770b7c3a68001dad6d3861ff43967))
+* **api:** tenant usage ([09a4d02](https://github.com/ArkHQ-io/ark-python/commit/09a4d02d5acdb3b2a20ca62930e7c644bc5968c9))
+
## 0.17.0 (2026-02-03)
Full Changelog: [v0.16.0...v0.17.0](https://github.com/ArkHQ-io/ark-python/compare/v0.16.0...v0.17.0)
diff --git a/pyproject.toml b/pyproject.toml
index 1fc344a..8c5da08 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "ark-email"
-version = "0.17.0"
+version = "0.18.0"
description = "The official Python library for the ark API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/ark/_version.py b/src/ark/_version.py
index 8bd9d9e..53cb1de 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.17.0" # x-release-please-version
+__version__ = "0.18.0" # x-release-please-version