diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d52d2b9..a26ebfc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.0" + ".": "0.14.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ebb8e0e..0976618 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 34 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-b651c88f3a87cebd528784fef019612f41517d0912c7e5695bb4419d00b9409b.yml -openapi_spec_hash: 33d0f5c2bb0349abf085404e2a3b05f7 -config_hash: fbd4e7a9ee50aad316893984a725519b +configured_endpoints: 35 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/ark%2Fark-1949bcfc8775c97eca880428dc93e9f97aa91144bef82584027ede5089bb2e19.yml +openapi_spec_hash: 0aa367455a067b701f18ef7892b6c7e9 +config_hash: 373e654f8034a40c42234eee9ebefbb9 diff --git a/CHANGELOG.md b/CHANGELOG.md index f186509..aabac85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.14.0 (2026-01-29) + +Full Changelog: [v0.13.0...v0.14.0](https://github.com/ArkHQ-io/ark-python/compare/v0.13.0...v0.14.0) + +### Features + +* **api:** add usage and SendLimit Headers ([4281980](https://github.com/ArkHQ-io/ark-python/commit/4281980353ff7423dbc6627e37abefee952bc489)) +* **api:** api update ([10f496a](https://github.com/ArkHQ-io/ark-python/commit/10f496a29924ecd9bcc93d0551ea8bd29662aa89)) +* **api:** domain list improvement ([6929689](https://github.com/ArkHQ-io/ark-python/commit/69296893c13ad8e4f05047f312009e88a8f02830)) + + +### Bug Fixes + +* **docs:** fix mcp installation instructions for remote servers ([f559107](https://github.com/ArkHQ-io/ark-python/commit/f55910772f06f551b98a1ddc7c7dda45fc2176f4)) + ## 0.13.0 (2026-01-25) Full Changelog: [v0.12.0...v0.13.0](https://github.com/ArkHQ-io/ark-python/compare/v0.12.0...v0.13.0) diff --git a/README.md b/README.md index 089c30b..bcf106d 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Ark MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6InNzZSIsInVybCI6Imh0dHBzOi8vYXJrLW1jcC5zdGxtY3AuY29tL3NzZSJ9) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%2Fsse%22%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=ark-email-mcp&config=eyJuYW1lIjoiYXJrLWVtYWlsLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Fyay1tY3Auc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcmstYXBpLWtleSI6Ik15IEFQSSBLZXkifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22ark-email-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fark-mcp.stlmcp.com%22%2C%22headers%22%3A%7B%22x-ark-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. diff --git a/api.md b/api.md index 4299dc0..d511272 100644 --- a/api.md +++ b/api.md @@ -142,3 +142,15 @@ Methods: - client.logs.retrieve(request_id) -> LogRetrieveResponse - client.logs.list(\*\*params) -> SyncPageNumberPagination[LogEntry] + +# Usage + +Types: + +```python +from ark.types import UsageRetrieveResponse +``` + +Methods: + +- client.usage.retrieve() -> UsageRetrieveResponse diff --git a/pyproject.toml b/pyproject.toml index fb937bb..0f30e15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ark-email" -version = "0.13.0" +version = "0.14.0" description = "The official Python library for the ark API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/ark/_client.py b/src/ark/_client.py index 31bf629..4a2f2a8 100644 --- a/src/ark/_client.py +++ b/src/ark/_client.py @@ -31,8 +31,9 @@ ) if TYPE_CHECKING: - from .resources import logs, emails, domains, tracking, webhooks, suppressions + from .resources import logs, usage, emails, domains, tracking, webhooks, suppressions from .resources.logs import LogsResource, AsyncLogsResource + from .resources.usage import UsageResource, AsyncUsageResource from .resources.emails import EmailsResource, AsyncEmailsResource from .resources.domains import DomainsResource, AsyncDomainsResource from .resources.tracking import TrackingResource, AsyncTrackingResource @@ -133,6 +134,12 @@ def logs(self) -> LogsResource: return LogsResource(self) + @cached_property + def usage(self) -> UsageResource: + from .resources.usage import UsageResource + + return UsageResource(self) + @cached_property def with_raw_response(self) -> ArkWithRawResponse: return ArkWithRawResponse(self) @@ -337,6 +344,12 @@ def logs(self) -> AsyncLogsResource: return AsyncLogsResource(self) + @cached_property + def usage(self) -> AsyncUsageResource: + from .resources.usage import AsyncUsageResource + + return AsyncUsageResource(self) + @cached_property def with_raw_response(self) -> AsyncArkWithRawResponse: return AsyncArkWithRawResponse(self) @@ -492,6 +505,12 @@ def logs(self) -> logs.LogsResourceWithRawResponse: return LogsResourceWithRawResponse(self._client.logs) + @cached_property + def usage(self) -> usage.UsageResourceWithRawResponse: + from .resources.usage import UsageResourceWithRawResponse + + return UsageResourceWithRawResponse(self._client.usage) + class AsyncArkWithRawResponse: _client: AsyncArk @@ -535,6 +554,12 @@ def logs(self) -> logs.AsyncLogsResourceWithRawResponse: return AsyncLogsResourceWithRawResponse(self._client.logs) + @cached_property + def usage(self) -> usage.AsyncUsageResourceWithRawResponse: + from .resources.usage import AsyncUsageResourceWithRawResponse + + return AsyncUsageResourceWithRawResponse(self._client.usage) + class ArkWithStreamedResponse: _client: Ark @@ -578,6 +603,12 @@ def logs(self) -> logs.LogsResourceWithStreamingResponse: return LogsResourceWithStreamingResponse(self._client.logs) + @cached_property + def usage(self) -> usage.UsageResourceWithStreamingResponse: + from .resources.usage import UsageResourceWithStreamingResponse + + return UsageResourceWithStreamingResponse(self._client.usage) + class AsyncArkWithStreamedResponse: _client: AsyncArk @@ -621,6 +652,12 @@ def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse: return AsyncLogsResourceWithStreamingResponse(self._client.logs) + @cached_property + def usage(self) -> usage.AsyncUsageResourceWithStreamingResponse: + from .resources.usage import AsyncUsageResourceWithStreamingResponse + + return AsyncUsageResourceWithStreamingResponse(self._client.usage) + Client = Ark diff --git a/src/ark/_version.py b/src/ark/_version.py index 8997200..9e3345c 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.13.0" # x-release-please-version +__version__ = "0.14.0" # x-release-please-version diff --git a/src/ark/resources/__init__.py b/src/ark/resources/__init__.py index 8bd1ac1..76b59c0 100644 --- a/src/ark/resources/__init__.py +++ b/src/ark/resources/__init__.py @@ -8,6 +8,14 @@ LogsResourceWithStreamingResponse, AsyncLogsResourceWithStreamingResponse, ) +from .usage import ( + UsageResource, + AsyncUsageResource, + UsageResourceWithRawResponse, + AsyncUsageResourceWithRawResponse, + UsageResourceWithStreamingResponse, + AsyncUsageResourceWithStreamingResponse, +) from .emails import ( EmailsResource, AsyncEmailsResource, @@ -86,4 +94,10 @@ "AsyncLogsResourceWithRawResponse", "LogsResourceWithStreamingResponse", "AsyncLogsResourceWithStreamingResponse", + "UsageResource", + "AsyncUsageResource", + "UsageResourceWithRawResponse", + "AsyncUsageResourceWithRawResponse", + "UsageResourceWithStreamingResponse", + "AsyncUsageResourceWithStreamingResponse", ] diff --git a/src/ark/resources/usage.py b/src/ark/resources/usage.py new file mode 100644 index 0000000..cb9004f --- /dev/null +++ b/src/ark/resources/usage.py @@ -0,0 +1,177 @@ +# 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.usage_retrieve_response import UsageRetrieveResponse + +__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, + *, + # 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 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 + """ + return self._get( + "/usage", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UsageRetrieveResponse, + ) + + +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, + *, + # 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 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 + """ + 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, + ) + + +class UsageResourceWithRawResponse: + def __init__(self, usage: UsageResource) -> None: + self._usage = usage + + self.retrieve = to_raw_response_wrapper( + usage.retrieve, + ) + + +class AsyncUsageResourceWithRawResponse: + def __init__(self, usage: AsyncUsageResource) -> None: + self._usage = usage + + self.retrieve = async_to_raw_response_wrapper( + usage.retrieve, + ) + + +class UsageResourceWithStreamingResponse: + def __init__(self, usage: UsageResource) -> None: + self._usage = usage + + self.retrieve = to_streamed_response_wrapper( + usage.retrieve, + ) + + +class AsyncUsageResourceWithStreamingResponse: + def __init__(self, usage: AsyncUsageResource) -> None: + self._usage = usage + + self.retrieve = async_to_streamed_response_wrapper( + usage.retrieve, + ) diff --git a/src/ark/resources/webhooks.py b/src/ark/resources/webhooks.py index f59179e..97b0202 100644 --- a/src/ark/resources/webhooks.py +++ b/src/ark/resources/webhooks.py @@ -70,8 +70,6 @@ def create( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] ] @@ -292,8 +290,6 @@ def list_deliveries( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] | Omit = omit, page: int | Omit = omit, @@ -478,8 +474,6 @@ def test( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ], # 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. @@ -563,8 +557,6 @@ async def create( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] ] @@ -785,8 +777,6 @@ async def list_deliveries( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] | Omit = omit, page: int | Omit = omit, @@ -971,8 +961,6 @@ async def test( "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ], # 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. diff --git a/src/ark/types/__init__.py b/src/ark/types/__init__.py index c543a4e..6cd75fb 100644 --- a/src/ark/types/__init__.py +++ b/src/ark/types/__init__.py @@ -33,6 +33,7 @@ from .email_send_batch_params import EmailSendBatchParams as EmailSendBatchParams from .email_send_raw_response import EmailSendRawResponse as EmailSendRawResponse from .suppression_list_params import SuppressionListParams as SuppressionListParams +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 diff --git a/src/ark/types/dns_record.py b/src/ark/types/dns_record.py index 01a9744..775e76e 100644 --- a/src/ark/types/dns_record.py +++ b/src/ark/types/dns_record.py @@ -3,26 +3,55 @@ from typing import Optional from typing_extensions import Literal +from pydantic import Field as FieldInfo + from .._models import BaseModel __all__ = ["DNSRecord"] class DNSRecord(BaseModel): + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider (which auto-appends the zone). + The `fullName` field contains the complete fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + full_name: str = FieldInfo(alias="fullName") + """ + The complete fully-qualified domain name (FQDN). Use this as a reference to + verify the record is configured correctly. + """ + name: str - """DNS record name (hostname)""" + """ + The relative hostname to enter in your DNS provider. Most DNS providers + auto-append the zone name, so you only need to enter this relative part. + + - `"@"` means the apex/root of the zone (for root domains) + - `"mail"` for a subdomain like `mail.example.com` + - `"ark-xyz._domainkey.mail"` for DKIM on a subdomain + """ type: Literal["TXT", "CNAME", "MX"] - """DNS record type""" + """The DNS record type to create""" value: str - """DNS record value""" + """The value to set for the DNS record""" status: Optional[Literal["OK", "Missing", "Invalid"]] = None - """DNS verification status: + """Current verification status of this DNS record: - - `OK` - Record is correctly configured - - `Missing` - Record not found in DNS - - `Invalid` - Record exists but has wrong value - - `null` - Not yet checked + - `OK` - Record is correctly configured and verified + - `Missing` - Record was not found in your DNS + - `Invalid` - Record exists but has an incorrect value + - `null` - Record has not been checked yet """ diff --git a/src/ark/types/domain_create_response.py b/src/ark/types/domain_create_response.py index 68ad482..56fdef4 100644 --- a/src/ark/types/domain_create_response.py +++ b/src/ark/types/domain_create_response.py @@ -14,31 +14,118 @@ class DataDNSRecords(BaseModel): - dkim: DNSRecord + """DNS records that must be added to your domain's DNS settings. - return_path: DNSRecord = FieldInfo(alias="returnPath") + Null if records are not yet generated. - spf: DNSRecord + **Important:** The `name` field contains the relative hostname that you should enter in your DNS provider. + Most DNS providers auto-append the zone name, so you only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ + + dkim: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + return_path: Optional[DNSRecord] = FieldInfo(alias="returnPath", default=None) + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + spf: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + zone: Optional[str] = None + """ + The DNS zone (registrable domain) where records should be added. This is the + root domain that your DNS provider manages. For `mail.example.com`, the zone is + `example.com`. For `example.co.uk`, the zone is `example.co.uk`. + """ class Data(BaseModel): - id: str - """Domain ID""" + id: int + """Unique domain identifier""" created_at: datetime = FieldInfo(alias="createdAt") + """Timestamp when the domain was added""" + + dns_records: Optional[DataDNSRecords] = FieldInfo(alias="dnsRecords", default=None) + """DNS records that must be added to your domain's DNS settings. + + Null if records are not yet generated. - dns_records: DataDNSRecords = FieldInfo(alias="dnsRecords") + **Important:** The `name` field contains the relative hostname that you should + enter in your DNS provider. Most DNS providers auto-append the zone name, so you + only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ name: str - """Domain name""" + """The domain name used for sending emails""" uuid: str + """UUID of the domain""" verified: bool - """Whether DNS is verified""" + """Whether all DNS records (SPF, DKIM, Return Path) are correctly configured. + + Domain must be verified before sending emails. + """ verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None) - """When the domain was verified (null if not verified)""" + """Timestamp when the domain ownership was verified, or null if not yet verified""" class DomainCreateResponse(BaseModel): diff --git a/src/ark/types/domain_list_response.py b/src/ark/types/domain_list_response.py index 25df99d..50edc86 100644 --- a/src/ark/types/domain_list_response.py +++ b/src/ark/types/domain_list_response.py @@ -3,8 +3,6 @@ from typing import List from typing_extensions import Literal -from pydantic import Field as FieldInfo - from .._models import BaseModel from .shared.api_meta import APIMeta @@ -12,14 +10,17 @@ class DataDomain(BaseModel): - id: str - """Domain ID""" - - dns_ok: bool = FieldInfo(alias="dnsOk") + id: int + """Unique domain identifier""" name: str + """The domain name used for sending emails""" verified: bool + """Whether all DNS records (SPF, DKIM, Return Path) are correctly configured. + + Domain must be verified before sending emails. + """ class Data(BaseModel): diff --git a/src/ark/types/domain_retrieve_response.py b/src/ark/types/domain_retrieve_response.py index 0c25702..bc9d550 100644 --- a/src/ark/types/domain_retrieve_response.py +++ b/src/ark/types/domain_retrieve_response.py @@ -14,31 +14,118 @@ class DataDNSRecords(BaseModel): - dkim: DNSRecord + """DNS records that must be added to your domain's DNS settings. - return_path: DNSRecord = FieldInfo(alias="returnPath") + Null if records are not yet generated. - spf: DNSRecord + **Important:** The `name` field contains the relative hostname that you should enter in your DNS provider. + Most DNS providers auto-append the zone name, so you only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ + + dkim: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + return_path: Optional[DNSRecord] = FieldInfo(alias="returnPath", default=None) + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + spf: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + zone: Optional[str] = None + """ + The DNS zone (registrable domain) where records should be added. This is the + root domain that your DNS provider manages. For `mail.example.com`, the zone is + `example.com`. For `example.co.uk`, the zone is `example.co.uk`. + """ class Data(BaseModel): - id: str - """Domain ID""" + id: int + """Unique domain identifier""" created_at: datetime = FieldInfo(alias="createdAt") + """Timestamp when the domain was added""" + + dns_records: Optional[DataDNSRecords] = FieldInfo(alias="dnsRecords", default=None) + """DNS records that must be added to your domain's DNS settings. + + Null if records are not yet generated. - dns_records: DataDNSRecords = FieldInfo(alias="dnsRecords") + **Important:** The `name` field contains the relative hostname that you should + enter in your DNS provider. Most DNS providers auto-append the zone name, so you + only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ name: str - """Domain name""" + """The domain name used for sending emails""" uuid: str + """UUID of the domain""" verified: bool - """Whether DNS is verified""" + """Whether all DNS records (SPF, DKIM, Return Path) are correctly configured. + + Domain must be verified before sending emails. + """ verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None) - """When the domain was verified (null if not verified)""" + """Timestamp when the domain ownership was verified, or null if not yet verified""" class DomainRetrieveResponse(BaseModel): diff --git a/src/ark/types/domain_verify_response.py b/src/ark/types/domain_verify_response.py index 63afdd7..8bc1ae9 100644 --- a/src/ark/types/domain_verify_response.py +++ b/src/ark/types/domain_verify_response.py @@ -14,31 +14,118 @@ class DataDNSRecords(BaseModel): - dkim: DNSRecord + """DNS records that must be added to your domain's DNS settings. - return_path: DNSRecord = FieldInfo(alias="returnPath") + Null if records are not yet generated. - spf: DNSRecord + **Important:** The `name` field contains the relative hostname that you should enter in your DNS provider. + Most DNS providers auto-append the zone name, so you only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ + + dkim: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + return_path: Optional[DNSRecord] = FieldInfo(alias="returnPath", default=None) + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + spf: Optional[DNSRecord] = None + """A DNS record that needs to be configured in your domain's DNS settings. + + The `name` field contains the relative hostname to enter in your DNS provider + (which auto-appends the zone). The `fullName` field contains the complete + fully-qualified domain name (FQDN) for reference. + + **Example for subdomain `mail.example.com`:** + + - `name`: `"mail"` (what you enter in DNS provider) + - `fullName`: `"mail.example.com"` (the complete hostname) + + **Example for root domain `example.com`:** + + - `name`: `"@"` (DNS shorthand for apex/root) + - `fullName`: `"example.com"` + """ + + zone: Optional[str] = None + """ + The DNS zone (registrable domain) where records should be added. This is the + root domain that your DNS provider manages. For `mail.example.com`, the zone is + `example.com`. For `example.co.uk`, the zone is `example.co.uk`. + """ class Data(BaseModel): - id: str - """Domain ID""" + id: int + """Unique domain identifier""" created_at: datetime = FieldInfo(alias="createdAt") + """Timestamp when the domain was added""" + + dns_records: Optional[DataDNSRecords] = FieldInfo(alias="dnsRecords", default=None) + """DNS records that must be added to your domain's DNS settings. + + Null if records are not yet generated. - dns_records: DataDNSRecords = FieldInfo(alias="dnsRecords") + **Important:** The `name` field contains the relative hostname that you should + enter in your DNS provider. Most DNS providers auto-append the zone name, so you + only need to enter the relative part. + + For subdomains like `mail.example.com`, the zone is `example.com`, so: + + - SPF `name` would be `mail` (not `@`) + - DKIM `name` would be `ark-xyz._domainkey.mail` + - Return Path `name` would be `psrp.mail` + """ name: str - """Domain name""" + """The domain name used for sending emails""" uuid: str + """UUID of the domain""" verified: bool - """Whether DNS is verified""" + """Whether all DNS records (SPF, DKIM, Return Path) are correctly configured. + + Domain must be verified before sending emails. + """ verified_at: Optional[datetime] = FieldInfo(alias="verifiedAt", default=None) - """When the domain was verified (null if not verified)""" + """Timestamp when the domain ownership was verified, or null if not yet verified""" class DomainVerifyResponse(BaseModel): diff --git a/src/ark/types/track_domain.py b/src/ark/types/track_domain.py index f630e82..f2786ea 100644 --- a/src/ark/types/track_domain.py +++ b/src/ark/types/track_domain.py @@ -32,7 +32,10 @@ class TrackDomain(BaseModel): """When the track domain was created""" dns_ok: bool = FieldInfo(alias="dnsOk") - """Whether DNS is correctly configured""" + """Whether the tracking CNAME record is correctly configured. + + Must be true to use tracking features. + """ domain_id: str = FieldInfo(alias="domainId") """ID of the parent sending domain""" diff --git a/src/ark/types/usage_retrieve_response.py b/src/ark/types/usage_retrieve_response.py new file mode 100644 index 0000000..86daa16 --- /dev/null +++ b/src/ark/types/usage_retrieve_response.py @@ -0,0 +1,109 @@ +# 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__ = ["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)""" + + +class UsageRetrieveResponse(BaseModel): + """Account usage and limits response""" + + data: Data + """Current usage and limit information""" + + meta: APIMeta + + success: Literal[True] diff --git a/src/ark/types/webhook_create_params.py b/src/ark/types/webhook_create_params.py index 8b7a63e..9899826 100644 --- a/src/ark/types/webhook_create_params.py +++ b/src/ark/types/webhook_create_params.py @@ -34,8 +34,6 @@ class WebhookCreateParams(TypedDict, total=False): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] ] diff --git a/src/ark/types/webhook_create_response.py b/src/ark/types/webhook_create_response.py index 07f75ec..d78bb58 100644 --- a/src/ark/types/webhook_create_response.py +++ b/src/ark/types/webhook_create_response.py @@ -34,8 +34,6 @@ class Data(BaseModel): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] """Subscribed events""" diff --git a/src/ark/types/webhook_list_deliveries_params.py b/src/ark/types/webhook_list_deliveries_params.py index cce2c61..c4048e4 100644 --- a/src/ark/types/webhook_list_deliveries_params.py +++ b/src/ark/types/webhook_list_deliveries_params.py @@ -25,8 +25,6 @@ class WebhookListDeliveriesParams(TypedDict, total=False): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] """Filter by event type""" diff --git a/src/ark/types/webhook_list_deliveries_response.py b/src/ark/types/webhook_list_deliveries_response.py index cb5033d..71a5fe2 100644 --- a/src/ark/types/webhook_list_deliveries_response.py +++ b/src/ark/types/webhook_list_deliveries_response.py @@ -30,8 +30,6 @@ class Data(BaseModel): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] """Event type that triggered this delivery""" diff --git a/src/ark/types/webhook_retrieve_delivery_response.py b/src/ark/types/webhook_retrieve_delivery_response.py index 9a0c38b..efec5c4 100644 --- a/src/ark/types/webhook_retrieve_delivery_response.py +++ b/src/ark/types/webhook_retrieve_delivery_response.py @@ -50,8 +50,6 @@ class Data(BaseModel): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] """Event type that triggered this delivery""" diff --git a/src/ark/types/webhook_retrieve_response.py b/src/ark/types/webhook_retrieve_response.py index db780ca..6cb2f2e 100644 --- a/src/ark/types/webhook_retrieve_response.py +++ b/src/ark/types/webhook_retrieve_response.py @@ -34,8 +34,6 @@ class Data(BaseModel): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] """Subscribed events""" diff --git a/src/ark/types/webhook_test_params.py b/src/ark/types/webhook_test_params.py index 5648540..3f91fca 100644 --- a/src/ark/types/webhook_test_params.py +++ b/src/ark/types/webhook_test_params.py @@ -18,8 +18,6 @@ class WebhookTestParams(TypedDict, total=False): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] """Event type to simulate""" diff --git a/src/ark/types/webhook_update_response.py b/src/ark/types/webhook_update_response.py index 75c6d59..4d9a0c6 100644 --- a/src/ark/types/webhook_update_response.py +++ b/src/ark/types/webhook_update_response.py @@ -34,8 +34,6 @@ class Data(BaseModel): "MessageLinkClicked", "MessageLoaded", "DomainDNSError", - "SendLimitApproaching", - "SendLimitExceeded", ] ] """Subscribed events""" diff --git a/tests/api_resources/test_usage.py b/tests/api_resources/test_usage.py new file mode 100644 index 0000000..767da80 --- /dev/null +++ b/tests/api_resources/test_usage.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 UsageRetrieveResponse +from tests.utils import assert_matches_type + +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.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() + + 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.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 + + +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.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() + + 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.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