epostix 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epostix/__init__.py +223 -0
- epostix/_client.py +253 -0
- epostix/_codec.py +248 -0
- epostix/_errors.py +335 -0
- epostix/_generated/__init__.py +19 -0
- epostix/_generated/codecs.py +1027 -0
- epostix/_generated/models.py +1268 -0
- epostix/_generated/operations.py +879 -0
- epostix/_generated/resources/__init__.py +90 -0
- epostix/_generated/resources/analytics.py +83 -0
- epostix/_generated/resources/api_keys.py +231 -0
- epostix/_generated/resources/attachments.py +15 -0
- epostix/_generated/resources/broadcasts.py +239 -0
- epostix/_generated/resources/contacts.py +296 -0
- epostix/_generated/resources/domains.py +193 -0
- epostix/_generated/resources/emails.py +346 -0
- epostix/_generated/resources/events.py +136 -0
- epostix/_generated/resources/inbound.py +169 -0
- epostix/_generated/resources/suppressions.py +186 -0
- epostix/_generated/resources/tags.py +119 -0
- epostix/_generated/resources/templates.py +202 -0
- epostix/_generated/resources/templates_versions.py +157 -0
- epostix/_generated/resources/webhooks.py +236 -0
- epostix/_generated/resources/webhooks_logs.py +150 -0
- epostix/_mailbox.py +23 -0
- epostix/_model.py +24 -0
- epostix/_pagination.py +293 -0
- epostix/_response.py +109 -0
- epostix/_retry.py +193 -0
- epostix/_transport.py +716 -0
- epostix/_types.py +20 -0
- epostix/attachments.py +152 -0
- epostix/batch.py +119 -0
- epostix/models.py +291 -0
- epostix/overrides.py +216 -0
- epostix/py.typed +0 -0
- epostix/webhooks.py +322 -0
- epostix-1.0.0.dist-info/METADATA +6 -0
- epostix-1.0.0.dist-info/RECORD +40 -0
- epostix-1.0.0.dist-info/WHEEL +4 -0
epostix/__init__.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from . import attachments as attachments
|
|
4
|
+
from . import batch as batch
|
|
5
|
+
from . import models as models
|
|
6
|
+
from . import overrides as overrides
|
|
7
|
+
from . import webhooks as webhooks
|
|
8
|
+
from ._client import (
|
|
9
|
+
DEDUPLICATION_SCOPE as DEDUPLICATION_SCOPE,
|
|
10
|
+
)
|
|
11
|
+
from ._client import (
|
|
12
|
+
PRODUCTION_BASE_URL as PRODUCTION_BASE_URL,
|
|
13
|
+
)
|
|
14
|
+
from ._client import (
|
|
15
|
+
SANDBOX_DOMAIN as SANDBOX_DOMAIN,
|
|
16
|
+
)
|
|
17
|
+
from ._client import (
|
|
18
|
+
STAGING_BASE_URL as STAGING_BASE_URL,
|
|
19
|
+
)
|
|
20
|
+
from ._client import (
|
|
21
|
+
AsyncEpostix as AsyncEpostix,
|
|
22
|
+
)
|
|
23
|
+
from ._client import (
|
|
24
|
+
Epostix as Epostix,
|
|
25
|
+
)
|
|
26
|
+
from ._client import (
|
|
27
|
+
KeyEnvironment as KeyEnvironment,
|
|
28
|
+
)
|
|
29
|
+
from ._client import (
|
|
30
|
+
key_environment_of as key_environment_of,
|
|
31
|
+
)
|
|
32
|
+
from ._codec import (
|
|
33
|
+
EpostixDecodeError as EpostixDecodeError,
|
|
34
|
+
)
|
|
35
|
+
from ._codec import (
|
|
36
|
+
is_known_enum_value as is_known_enum_value,
|
|
37
|
+
)
|
|
38
|
+
from ._errors import (
|
|
39
|
+
EpostixAccountSuspendedError as EpostixAccountSuspendedError,
|
|
40
|
+
)
|
|
41
|
+
from ._errors import (
|
|
42
|
+
EpostixAPIError as EpostixAPIError,
|
|
43
|
+
)
|
|
44
|
+
from ._errors import (
|
|
45
|
+
EpostixAuthenticationError as EpostixAuthenticationError,
|
|
46
|
+
)
|
|
47
|
+
from ._errors import (
|
|
48
|
+
EpostixConfigurationError as EpostixConfigurationError,
|
|
49
|
+
)
|
|
50
|
+
from ._errors import (
|
|
51
|
+
EpostixConflictError as EpostixConflictError,
|
|
52
|
+
)
|
|
53
|
+
from ._errors import (
|
|
54
|
+
EpostixContentError as EpostixContentError,
|
|
55
|
+
)
|
|
56
|
+
from ._errors import (
|
|
57
|
+
EpostixError as EpostixError,
|
|
58
|
+
)
|
|
59
|
+
from ._errors import (
|
|
60
|
+
EpostixErrorType as EpostixErrorType,
|
|
61
|
+
)
|
|
62
|
+
from ._errors import (
|
|
63
|
+
EpostixIdempotencyConflictError as EpostixIdempotencyConflictError,
|
|
64
|
+
)
|
|
65
|
+
from ._errors import (
|
|
66
|
+
EpostixIdempotencyInProgressError as EpostixIdempotencyInProgressError,
|
|
67
|
+
)
|
|
68
|
+
from ._errors import (
|
|
69
|
+
EpostixInvalidRequestError as EpostixInvalidRequestError,
|
|
70
|
+
)
|
|
71
|
+
from ._errors import (
|
|
72
|
+
EpostixNotFoundError as EpostixNotFoundError,
|
|
73
|
+
)
|
|
74
|
+
from ._errors import (
|
|
75
|
+
EpostixOutcomeUnknownError as EpostixOutcomeUnknownError,
|
|
76
|
+
)
|
|
77
|
+
from ._errors import (
|
|
78
|
+
EpostixPaginationError as EpostixPaginationError,
|
|
79
|
+
)
|
|
80
|
+
from ._errors import (
|
|
81
|
+
EpostixPermissionError as EpostixPermissionError,
|
|
82
|
+
)
|
|
83
|
+
from ._errors import (
|
|
84
|
+
EpostixQuotaExceededError as EpostixQuotaExceededError,
|
|
85
|
+
)
|
|
86
|
+
from ._errors import (
|
|
87
|
+
EpostixRateLimitError as EpostixRateLimitError,
|
|
88
|
+
)
|
|
89
|
+
from ._errors import (
|
|
90
|
+
EpostixServerError as EpostixServerError,
|
|
91
|
+
)
|
|
92
|
+
from ._errors import (
|
|
93
|
+
EpostixTimeoutError as EpostixTimeoutError,
|
|
94
|
+
)
|
|
95
|
+
from ._errors import (
|
|
96
|
+
EpostixTransportError as EpostixTransportError,
|
|
97
|
+
)
|
|
98
|
+
from ._errors import (
|
|
99
|
+
EpostixValidationError as EpostixValidationError,
|
|
100
|
+
)
|
|
101
|
+
from ._errors import (
|
|
102
|
+
EpostixWebhookVerificationError as EpostixWebhookVerificationError,
|
|
103
|
+
)
|
|
104
|
+
from ._errors import (
|
|
105
|
+
FieldError as FieldError,
|
|
106
|
+
)
|
|
107
|
+
from ._errors import (
|
|
108
|
+
PaginationFailureReason as PaginationFailureReason,
|
|
109
|
+
)
|
|
110
|
+
from ._errors import (
|
|
111
|
+
QuotaPeriod as QuotaPeriod,
|
|
112
|
+
)
|
|
113
|
+
from ._errors import (
|
|
114
|
+
TimeoutPhase as TimeoutPhase,
|
|
115
|
+
)
|
|
116
|
+
from ._errors import (
|
|
117
|
+
WebhookFailureReason as WebhookFailureReason,
|
|
118
|
+
)
|
|
119
|
+
from ._mailbox import (
|
|
120
|
+
Mailbox as Mailbox,
|
|
121
|
+
)
|
|
122
|
+
from ._mailbox import (
|
|
123
|
+
addr as addr,
|
|
124
|
+
)
|
|
125
|
+
from ._mailbox import (
|
|
126
|
+
named as named,
|
|
127
|
+
)
|
|
128
|
+
from ._model import (
|
|
129
|
+
Model as Model,
|
|
130
|
+
)
|
|
131
|
+
from ._model import (
|
|
132
|
+
response_metadata_of as response_metadata_of,
|
|
133
|
+
)
|
|
134
|
+
from ._model import (
|
|
135
|
+
unknown_fields_of as unknown_fields_of,
|
|
136
|
+
)
|
|
137
|
+
from ._pagination import (
|
|
138
|
+
AsyncPaginator as AsyncPaginator,
|
|
139
|
+
)
|
|
140
|
+
from ._pagination import (
|
|
141
|
+
Collection as Collection,
|
|
142
|
+
)
|
|
143
|
+
from ._pagination import (
|
|
144
|
+
Page as Page,
|
|
145
|
+
)
|
|
146
|
+
from ._pagination import (
|
|
147
|
+
Paginator as Paginator,
|
|
148
|
+
)
|
|
149
|
+
from ._response import (
|
|
150
|
+
AttemptRecord as AttemptRecord,
|
|
151
|
+
)
|
|
152
|
+
from ._response import (
|
|
153
|
+
RateLimitSnapshot as RateLimitSnapshot,
|
|
154
|
+
)
|
|
155
|
+
from ._response import (
|
|
156
|
+
ResponseMetadata as ResponseMetadata,
|
|
157
|
+
)
|
|
158
|
+
from ._response import (
|
|
159
|
+
RetriesExhaustedReason as RetriesExhaustedReason,
|
|
160
|
+
)
|
|
161
|
+
from ._retry import (
|
|
162
|
+
BATCH_RETRY_POLICY as BATCH_RETRY_POLICY,
|
|
163
|
+
)
|
|
164
|
+
from ._retry import (
|
|
165
|
+
DEFAULT_RETRY_POLICY as DEFAULT_RETRY_POLICY,
|
|
166
|
+
)
|
|
167
|
+
from ._retry import (
|
|
168
|
+
DISABLED_RETRY_POLICY as DISABLED_RETRY_POLICY,
|
|
169
|
+
)
|
|
170
|
+
from ._retry import (
|
|
171
|
+
UPLOAD_RETRY_POLICY as UPLOAD_RETRY_POLICY,
|
|
172
|
+
)
|
|
173
|
+
from ._retry import (
|
|
174
|
+
MutationRetryMode as MutationRetryMode,
|
|
175
|
+
)
|
|
176
|
+
from ._retry import (
|
|
177
|
+
RetryClass as RetryClass,
|
|
178
|
+
)
|
|
179
|
+
from ._retry import (
|
|
180
|
+
RetryPolicy as RetryPolicy,
|
|
181
|
+
)
|
|
182
|
+
from ._transport import (
|
|
183
|
+
IDEMPOTENCY_KEY_MAXIMUM_LENGTH as IDEMPOTENCY_KEY_MAXIMUM_LENGTH,
|
|
184
|
+
)
|
|
185
|
+
from ._transport import (
|
|
186
|
+
IDEMPOTENCY_RETENTION_HOURS as IDEMPOTENCY_RETENTION_HOURS,
|
|
187
|
+
)
|
|
188
|
+
from ._transport import (
|
|
189
|
+
OperationSpec as OperationSpec,
|
|
190
|
+
)
|
|
191
|
+
from ._transport import (
|
|
192
|
+
RequestOptions as RequestOptions,
|
|
193
|
+
)
|
|
194
|
+
from .attachments import (
|
|
195
|
+
ATTACHMENT_DECODED_LIMIT_BYTES as ATTACHMENT_DECODED_LIMIT_BYTES,
|
|
196
|
+
)
|
|
197
|
+
from .attachments import (
|
|
198
|
+
REQUEST_BODY_LIMIT_BYTES as REQUEST_BODY_LIMIT_BYTES,
|
|
199
|
+
)
|
|
200
|
+
from .attachments import (
|
|
201
|
+
EpostixAttachmentTooLargeError as EpostixAttachmentTooLargeError,
|
|
202
|
+
)
|
|
203
|
+
from .attachments import (
|
|
204
|
+
EpostixRequestTooLargeError as EpostixRequestTooLargeError,
|
|
205
|
+
)
|
|
206
|
+
from ._types import (
|
|
207
|
+
NOT_GIVEN as NOT_GIVEN,
|
|
208
|
+
)
|
|
209
|
+
from ._types import (
|
|
210
|
+
JsonValue as JsonValue,
|
|
211
|
+
)
|
|
212
|
+
from ._types import (
|
|
213
|
+
NotGiven as NotGiven,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
from .overrides import (
|
|
217
|
+
AsyncRawContent as AsyncRawContent,
|
|
218
|
+
)
|
|
219
|
+
from .overrides import (
|
|
220
|
+
RawContent as RawContent,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
__version__ = "1.0.0"
|
epostix/_client.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random as _random
|
|
4
|
+
import time
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Callable, Literal, Mapping, TypeAlias
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._errors import EpostixConfigurationError
|
|
12
|
+
from ._generated.resources import build_async_resources, build_resources
|
|
13
|
+
from ._generated.resources.analytics import AnalyticsResource, AsyncAnalyticsResource
|
|
14
|
+
from ._generated.resources.api_keys import ApiKeysResource, AsyncApiKeysResource
|
|
15
|
+
from ._generated.resources.broadcasts import AsyncBroadcastsResource, BroadcastsResource
|
|
16
|
+
from ._generated.resources.contacts import AsyncContactsResource, ContactsResource
|
|
17
|
+
from ._generated.resources.domains import AsyncDomainsResource, DomainsResource
|
|
18
|
+
from ._generated.resources.emails import AsyncEmailsResource, EmailsResource
|
|
19
|
+
from ._generated.resources.events import AsyncEventsResource, EventsResource
|
|
20
|
+
from ._generated.resources.suppressions import AsyncSuppressionsResource, SuppressionsResource
|
|
21
|
+
from ._generated.resources.tags import AsyncTagsResource, TagsResource
|
|
22
|
+
from ._generated.resources.templates import AsyncTemplatesResource, TemplatesResource
|
|
23
|
+
from ._generated.resources.webhooks import AsyncWebhooksResource, WebhooksResource
|
|
24
|
+
from ._retry import DEFAULT_RETRY_POLICY, RetryPolicy
|
|
25
|
+
from ._transport import (
|
|
26
|
+
AsyncTransport,
|
|
27
|
+
TransportConfig,
|
|
28
|
+
Transport,
|
|
29
|
+
build_async_http_client,
|
|
30
|
+
build_http_client,
|
|
31
|
+
)
|
|
32
|
+
from .overrides import (
|
|
33
|
+
AsyncAttachmentsResource,
|
|
34
|
+
AsyncInboundResource,
|
|
35
|
+
AttachmentsResource,
|
|
36
|
+
InboundResource,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
PRODUCTION_BASE_URL = "https://api.epostix.com/v1"
|
|
40
|
+
STAGING_BASE_URL = "https://api.staging.epostix.com/v1"
|
|
41
|
+
SANDBOX_DOMAIN = "epostix.dev"
|
|
42
|
+
DEDUPLICATION_SCOPE = "api_key + environment + method + path + key, 24h"
|
|
43
|
+
USER_AGENT = "epostix-python/1.0.0"
|
|
44
|
+
|
|
45
|
+
KeyEnvironment: TypeAlias = Literal["live", "test", "unrecognized"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def key_environment_of(api_key: str) -> KeyEnvironment:
|
|
49
|
+
if api_key.startswith("tix_live_"):
|
|
50
|
+
return "live"
|
|
51
|
+
|
|
52
|
+
if api_key.startswith("tix_test_"):
|
|
53
|
+
return "test"
|
|
54
|
+
|
|
55
|
+
return "unrecognized"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _build_config(
|
|
59
|
+
api_key: str,
|
|
60
|
+
base_url: str,
|
|
61
|
+
retry_policy: RetryPolicy,
|
|
62
|
+
default_headers: Mapping[str, str] | None,
|
|
63
|
+
user_agent_suffix: str | None,
|
|
64
|
+
automatic_idempotency_keys: bool,
|
|
65
|
+
random: Callable[[], float] | None,
|
|
66
|
+
now: Callable[[], float] | None,
|
|
67
|
+
) -> TransportConfig:
|
|
68
|
+
if not api_key or not api_key.strip():
|
|
69
|
+
raise EpostixConfigurationError("an API key is required")
|
|
70
|
+
|
|
71
|
+
if api_key.startswith("Bearer "):
|
|
72
|
+
raise EpostixConfigurationError(
|
|
73
|
+
"pass the API key alone; the client adds the Bearer prefix itself"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parsed = urlparse(base_url)
|
|
77
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
78
|
+
raise EpostixConfigurationError(f"base_url is not a valid URL: {base_url}")
|
|
79
|
+
|
|
80
|
+
return TransportConfig(
|
|
81
|
+
api_key=api_key,
|
|
82
|
+
base_url=base_url.rstrip("/"),
|
|
83
|
+
retry_policy=retry_policy,
|
|
84
|
+
user_agent=USER_AGENT + (f" {user_agent_suffix}" if user_agent_suffix else ""),
|
|
85
|
+
automatic_idempotency_keys=automatic_idempotency_keys,
|
|
86
|
+
default_headers=dict(default_headers or {}),
|
|
87
|
+
random=random or _random.random,
|
|
88
|
+
now=now or time.monotonic,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Epostix:
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
api_key: str,
|
|
96
|
+
*,
|
|
97
|
+
base_url: str = PRODUCTION_BASE_URL,
|
|
98
|
+
retry_policy: RetryPolicy = DEFAULT_RETRY_POLICY,
|
|
99
|
+
http_client: httpx.Client | None = None,
|
|
100
|
+
default_headers: Mapping[str, str] | None = None,
|
|
101
|
+
user_agent_suffix: str | None = None,
|
|
102
|
+
automatic_idempotency_keys: bool = True,
|
|
103
|
+
random: Callable[[], float] | None = None,
|
|
104
|
+
now: Callable[[], float] | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
config = _build_config(
|
|
107
|
+
api_key,
|
|
108
|
+
base_url,
|
|
109
|
+
retry_policy,
|
|
110
|
+
default_headers,
|
|
111
|
+
user_agent_suffix,
|
|
112
|
+
automatic_idempotency_keys,
|
|
113
|
+
random,
|
|
114
|
+
now,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
owns_client = http_client is None
|
|
118
|
+
client = http_client if http_client is not None else build_http_client(retry_policy)
|
|
119
|
+
|
|
120
|
+
self._transport = Transport(config, client, owns_client)
|
|
121
|
+
resources = build_resources(self._transport)
|
|
122
|
+
|
|
123
|
+
self.analytics: AnalyticsResource = resources.analytics
|
|
124
|
+
self.api_keys: ApiKeysResource = resources.api_keys
|
|
125
|
+
self.attachments = AttachmentsResource(self._transport)
|
|
126
|
+
self.broadcasts: BroadcastsResource = resources.broadcasts
|
|
127
|
+
self.contacts: ContactsResource = resources.contacts
|
|
128
|
+
self.domains: DomainsResource = resources.domains
|
|
129
|
+
self.emails: EmailsResource = resources.emails
|
|
130
|
+
self.events: EventsResource = resources.events
|
|
131
|
+
self.inbound = InboundResource(self._transport)
|
|
132
|
+
self.suppressions: SuppressionsResource = resources.suppressions
|
|
133
|
+
self.tags: TagsResource = resources.tags
|
|
134
|
+
self.templates: TemplatesResource = resources.templates
|
|
135
|
+
self.webhooks: WebhooksResource = resources.webhooks
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def key_environment(self) -> KeyEnvironment:
|
|
139
|
+
return key_environment_of(self._transport.config.api_key)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def deduplication_scope(self) -> str:
|
|
143
|
+
return DEDUPLICATION_SCOPE
|
|
144
|
+
|
|
145
|
+
def with_api_key(self, api_key: str) -> Epostix:
|
|
146
|
+
config = self._transport.config
|
|
147
|
+
|
|
148
|
+
return Epostix(
|
|
149
|
+
api_key,
|
|
150
|
+
base_url=config.base_url,
|
|
151
|
+
retry_policy=config.retry_policy,
|
|
152
|
+
http_client=self._transport.http_client,
|
|
153
|
+
default_headers=config.default_headers,
|
|
154
|
+
automatic_idempotency_keys=config.automatic_idempotency_keys,
|
|
155
|
+
random=config.random,
|
|
156
|
+
now=config.now,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def close(self) -> None:
|
|
160
|
+
self._transport.close()
|
|
161
|
+
|
|
162
|
+
def __enter__(self) -> Epostix:
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(
|
|
166
|
+
self,
|
|
167
|
+
exc_type: type[BaseException] | None,
|
|
168
|
+
exc: BaseException | None,
|
|
169
|
+
traceback: TracebackType | None,
|
|
170
|
+
) -> None:
|
|
171
|
+
self.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class AsyncEpostix:
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
api_key: str,
|
|
178
|
+
*,
|
|
179
|
+
base_url: str = PRODUCTION_BASE_URL,
|
|
180
|
+
retry_policy: RetryPolicy = DEFAULT_RETRY_POLICY,
|
|
181
|
+
http_client: httpx.AsyncClient | None = None,
|
|
182
|
+
default_headers: Mapping[str, str] | None = None,
|
|
183
|
+
user_agent_suffix: str | None = None,
|
|
184
|
+
automatic_idempotency_keys: bool = True,
|
|
185
|
+
random: Callable[[], float] | None = None,
|
|
186
|
+
now: Callable[[], float] | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
config = _build_config(
|
|
189
|
+
api_key,
|
|
190
|
+
base_url,
|
|
191
|
+
retry_policy,
|
|
192
|
+
default_headers,
|
|
193
|
+
user_agent_suffix,
|
|
194
|
+
automatic_idempotency_keys,
|
|
195
|
+
random,
|
|
196
|
+
now,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
owns_client = http_client is None
|
|
200
|
+
client = http_client if http_client is not None else build_async_http_client(retry_policy)
|
|
201
|
+
|
|
202
|
+
self._transport = AsyncTransport(config, client, owns_client)
|
|
203
|
+
resources = build_async_resources(self._transport)
|
|
204
|
+
|
|
205
|
+
self.analytics: AsyncAnalyticsResource = resources.analytics
|
|
206
|
+
self.api_keys: AsyncApiKeysResource = resources.api_keys
|
|
207
|
+
self.attachments = AsyncAttachmentsResource(self._transport)
|
|
208
|
+
self.broadcasts: AsyncBroadcastsResource = resources.broadcasts
|
|
209
|
+
self.contacts: AsyncContactsResource = resources.contacts
|
|
210
|
+
self.domains: AsyncDomainsResource = resources.domains
|
|
211
|
+
self.emails: AsyncEmailsResource = resources.emails
|
|
212
|
+
self.events: AsyncEventsResource = resources.events
|
|
213
|
+
self.inbound = AsyncInboundResource(self._transport)
|
|
214
|
+
self.suppressions: AsyncSuppressionsResource = resources.suppressions
|
|
215
|
+
self.tags: AsyncTagsResource = resources.tags
|
|
216
|
+
self.templates: AsyncTemplatesResource = resources.templates
|
|
217
|
+
self.webhooks: AsyncWebhooksResource = resources.webhooks
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def key_environment(self) -> KeyEnvironment:
|
|
221
|
+
return key_environment_of(self._transport.config.api_key)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def deduplication_scope(self) -> str:
|
|
225
|
+
return DEDUPLICATION_SCOPE
|
|
226
|
+
|
|
227
|
+
def with_api_key(self, api_key: str) -> AsyncEpostix:
|
|
228
|
+
config = self._transport.config
|
|
229
|
+
|
|
230
|
+
return AsyncEpostix(
|
|
231
|
+
api_key,
|
|
232
|
+
base_url=config.base_url,
|
|
233
|
+
retry_policy=config.retry_policy,
|
|
234
|
+
http_client=self._transport.http_client,
|
|
235
|
+
default_headers=config.default_headers,
|
|
236
|
+
automatic_idempotency_keys=config.automatic_idempotency_keys,
|
|
237
|
+
random=config.random,
|
|
238
|
+
now=config.now,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def aclose(self) -> None:
|
|
242
|
+
await self._transport.aclose()
|
|
243
|
+
|
|
244
|
+
async def __aenter__(self) -> AsyncEpostix:
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
async def __aexit__(
|
|
248
|
+
self,
|
|
249
|
+
exc_type: type[BaseException] | None,
|
|
250
|
+
exc: BaseException | None,
|
|
251
|
+
traceback: TracebackType | None,
|
|
252
|
+
) -> None:
|
|
253
|
+
await self.aclose()
|
epostix/_codec.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Callable, Sequence
|
|
8
|
+
|
|
9
|
+
from ._errors import EpostixConfigurationError, EpostixError
|
|
10
|
+
from ._mailbox import Mailbox
|
|
11
|
+
from ._model import Model
|
|
12
|
+
from ._types import NOT_GIVEN, NotGiven
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EpostixDecodeError(EpostixError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Codec(ABC):
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def encode(self, value: Any) -> Any: ...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def decode(self, value: Any) -> Any: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Passthrough(Codec):
|
|
28
|
+
def encode(self, value: Any) -> Any:
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
def decode(self, value: Any) -> Any:
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def format_datetime(value: datetime) -> str:
|
|
36
|
+
text = value.isoformat()
|
|
37
|
+
|
|
38
|
+
if text.endswith("+00:00"):
|
|
39
|
+
return text[:-6] + "Z"
|
|
40
|
+
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _DateTime(Codec):
|
|
45
|
+
def encode(self, value: Any) -> Any:
|
|
46
|
+
if isinstance(value, datetime):
|
|
47
|
+
return format_datetime(value)
|
|
48
|
+
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
def decode(self, value: Any) -> Any:
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
try:
|
|
54
|
+
return datetime.fromisoformat(value)
|
|
55
|
+
except ValueError:
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
PASSTHROUGH: Codec = _Passthrough()
|
|
62
|
+
DATETIME: Codec = _DateTime()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ArrayCodec(Codec):
|
|
66
|
+
def __init__(self, inner: Callable[[], Codec]) -> None:
|
|
67
|
+
self._inner = inner
|
|
68
|
+
|
|
69
|
+
def encode(self, value: Any) -> Any:
|
|
70
|
+
if isinstance(value, (list, tuple)):
|
|
71
|
+
inner = self._inner()
|
|
72
|
+
return [inner.encode(item) for item in value]
|
|
73
|
+
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
def decode(self, value: Any) -> Any:
|
|
77
|
+
if isinstance(value, list):
|
|
78
|
+
inner = self._inner()
|
|
79
|
+
return [inner.decode(item) for item in value]
|
|
80
|
+
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MapCodec(Codec):
|
|
85
|
+
def __init__(self, inner: Callable[[], Codec]) -> None:
|
|
86
|
+
self._inner = inner
|
|
87
|
+
|
|
88
|
+
def encode(self, value: Any) -> Any:
|
|
89
|
+
if isinstance(value, dict):
|
|
90
|
+
inner = self._inner()
|
|
91
|
+
return {key: inner.encode(item) for key, item in value.items()}
|
|
92
|
+
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
def decode(self, value: Any) -> Any:
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
inner = self._inner()
|
|
98
|
+
return {key: inner.decode(item) for key, item in value.items()}
|
|
99
|
+
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class EnumCodec(Codec):
|
|
104
|
+
def __init__(self, enum_class: type[Enum]) -> None:
|
|
105
|
+
self._enum_class = enum_class
|
|
106
|
+
|
|
107
|
+
def encode(self, value: Any) -> Any:
|
|
108
|
+
if isinstance(value, Enum):
|
|
109
|
+
return value.value
|
|
110
|
+
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
def decode(self, value: Any) -> Any:
|
|
114
|
+
if not isinstance(value, str):
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return self._enum_class(value)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_known_enum_value(enum_class: type[Enum], value: str) -> bool:
|
|
124
|
+
try:
|
|
125
|
+
enum_class(value)
|
|
126
|
+
except ValueError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ScalarOrArrayCodec(Codec):
|
|
133
|
+
def encode(self, value: Any) -> Any:
|
|
134
|
+
if isinstance(value, str):
|
|
135
|
+
return [value]
|
|
136
|
+
|
|
137
|
+
if isinstance(value, (list, tuple)):
|
|
138
|
+
return list(value)
|
|
139
|
+
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
def decode(self, value: Any) -> Any:
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class MailboxCodec(Codec):
|
|
147
|
+
def encode(self, value: Any) -> Any:
|
|
148
|
+
if isinstance(value, Mailbox):
|
|
149
|
+
if not value.name:
|
|
150
|
+
return value.email
|
|
151
|
+
|
|
152
|
+
return {"email": value.email, "name": value.name}
|
|
153
|
+
|
|
154
|
+
return value
|
|
155
|
+
|
|
156
|
+
def decode(self, value: Any) -> Any:
|
|
157
|
+
if isinstance(value, dict):
|
|
158
|
+
return Mailbox(email=str(value.get("email", "")), name=value.get("name"))
|
|
159
|
+
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class FieldSpec:
|
|
165
|
+
wire: str
|
|
166
|
+
name: str
|
|
167
|
+
codec: Callable[[], Codec]
|
|
168
|
+
required: bool
|
|
169
|
+
nullable: bool
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ModelCodec(Codec):
|
|
173
|
+
def __init__(self, model_class: type[Model], fields: Sequence[FieldSpec]) -> None:
|
|
174
|
+
self._model_class = model_class
|
|
175
|
+
self._fields = fields
|
|
176
|
+
self._known = frozenset(spec.wire for spec in fields)
|
|
177
|
+
|
|
178
|
+
def encode(self, value: Any) -> Any:
|
|
179
|
+
if not isinstance(value, self._model_class):
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
out: dict[str, Any] = {}
|
|
183
|
+
|
|
184
|
+
for spec in self._fields:
|
|
185
|
+
raw = getattr(value, spec.name, NOT_GIVEN)
|
|
186
|
+
|
|
187
|
+
if isinstance(raw, NotGiven):
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
out[spec.wire] = None if raw is None else spec.codec().encode(raw)
|
|
191
|
+
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
def decode(self, value: Any) -> Any:
|
|
195
|
+
if not isinstance(value, dict):
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
instance = self._model_class.__new__(self._model_class)
|
|
199
|
+
|
|
200
|
+
for spec in self._fields:
|
|
201
|
+
if spec.wire in value:
|
|
202
|
+
raw = value[spec.wire]
|
|
203
|
+
setattr(
|
|
204
|
+
instance,
|
|
205
|
+
spec.name,
|
|
206
|
+
None if raw is None else spec.codec().decode(raw),
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
setattr(instance, spec.name, NOT_GIVEN)
|
|
210
|
+
|
|
211
|
+
instance.unknown_fields = {
|
|
212
|
+
key: item for key, item in value.items() if key not in self._known
|
|
213
|
+
}
|
|
214
|
+
instance.response_metadata = None
|
|
215
|
+
|
|
216
|
+
return instance
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class VariantSpec:
|
|
221
|
+
discriminator: str
|
|
222
|
+
model_class: type[Model]
|
|
223
|
+
codec: Callable[[], Codec]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class VariantCodec(Codec):
|
|
227
|
+
def __init__(self, variants: Sequence[VariantSpec], name: str) -> None:
|
|
228
|
+
self._variants = variants
|
|
229
|
+
self._name = name
|
|
230
|
+
|
|
231
|
+
def encode(self, value: Any) -> Any:
|
|
232
|
+
for variant in self._variants:
|
|
233
|
+
if isinstance(value, variant.model_class):
|
|
234
|
+
return variant.codec().encode(value)
|
|
235
|
+
|
|
236
|
+
raise EpostixConfigurationError(f"value does not match any {self._name} variant")
|
|
237
|
+
|
|
238
|
+
def decode(self, value: Any) -> Any:
|
|
239
|
+
if not isinstance(value, dict):
|
|
240
|
+
return value
|
|
241
|
+
|
|
242
|
+
for variant in self._variants:
|
|
243
|
+
if variant.discriminator in value:
|
|
244
|
+
return variant.codec().decode(value)
|
|
245
|
+
|
|
246
|
+
raise EpostixDecodeError(
|
|
247
|
+
f"the server returned a {self._name} shape this version does not recognise"
|
|
248
|
+
)
|