coffeemail 0.1.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.
coffeemail/__init__.py ADDED
@@ -0,0 +1,106 @@
1
+ from coffeemail.async_client import AsyncCoffeeMail
2
+ from coffeemail.client import CoffeeMail
3
+ from coffeemail.core.errors import (
4
+ AuthenticationError,
5
+ CoffeeMailError,
6
+ ConflictError,
7
+ NetworkError,
8
+ NotFoundError,
9
+ PermissionDeniedError,
10
+ RateLimitError,
11
+ ServerError,
12
+ ValidationError,
13
+ )
14
+ from coffeemail.core.types import ApiKeyIntrospection, CoffeeMailResponse
15
+ from coffeemail.models import (
16
+ AudienceDetail,
17
+ BatchSendEmailResult,
18
+ BroadcastDetail,
19
+ BroadcastStatus,
20
+ BulkAddContactsResponse,
21
+ CheckSuppressionResponse,
22
+ ContactDetail,
23
+ CreateDomainResponse,
24
+ CreatedWebhookDetail,
25
+ DnsRecord,
26
+ DomainDetail,
27
+ DomainStatus,
28
+ EmailAttachment,
29
+ EmailDetail,
30
+ EmailParticipant,
31
+ EmailStats,
32
+ EmailStatus,
33
+ EmailTag,
34
+ ListAudiencesResponse,
35
+ ListBroadcastsResponse,
36
+ ListContactsResponse,
37
+ ListDomainsResponse,
38
+ ListEmailsResponse,
39
+ ListSuppressionsResponse,
40
+ ListTemplatesResponse,
41
+ ListWebhooksResponse,
42
+ PreviewTemplateResponse,
43
+ RotateWebhookSecretResult,
44
+ SendEmailPayload,
45
+ SendEmailResponse,
46
+ SuppressionDetail,
47
+ SuppressionReason,
48
+ TemplateDetail,
49
+ TestWebhookResult,
50
+ WebhookDetail,
51
+ WebhookEvent,
52
+ )
53
+
54
+ __version__ = "0.1.0"
55
+
56
+ __all__ = [
57
+ "CoffeeMail",
58
+ "AsyncCoffeeMail",
59
+ "CoffeeMailResponse",
60
+ "ApiKeyIntrospection",
61
+ "CoffeeMailError",
62
+ "AuthenticationError",
63
+ "PermissionDeniedError",
64
+ "NotFoundError",
65
+ "ValidationError",
66
+ "ConflictError",
67
+ "RateLimitError",
68
+ "ServerError",
69
+ "NetworkError",
70
+ "BatchSendEmailResult",
71
+ "EmailAttachment",
72
+ "EmailDetail",
73
+ "EmailParticipant",
74
+ "EmailStatus",
75
+ "EmailTag",
76
+ "ListEmailsResponse",
77
+ "SendEmailPayload",
78
+ "SendEmailResponse",
79
+ "CreateDomainResponse",
80
+ "DnsRecord",
81
+ "DomainDetail",
82
+ "DomainStatus",
83
+ "ListDomainsResponse",
84
+ "ListTemplatesResponse",
85
+ "PreviewTemplateResponse",
86
+ "TemplateDetail",
87
+ "AudienceDetail",
88
+ "BulkAddContactsResponse",
89
+ "ContactDetail",
90
+ "ListAudiencesResponse",
91
+ "ListContactsResponse",
92
+ "BroadcastDetail",
93
+ "BroadcastStatus",
94
+ "ListBroadcastsResponse",
95
+ "CheckSuppressionResponse",
96
+ "ListSuppressionsResponse",
97
+ "SuppressionDetail",
98
+ "SuppressionReason",
99
+ "CreatedWebhookDetail",
100
+ "ListWebhooksResponse",
101
+ "RotateWebhookSecretResult",
102
+ "TestWebhookResult",
103
+ "WebhookDetail",
104
+ "WebhookEvent",
105
+ "EmailStats",
106
+ ]
@@ -0,0 +1,52 @@
1
+ import os
2
+
3
+ import httpx
4
+
5
+ from coffeemail.core.http import DEFAULT_TIMEOUT_SECONDS, AsyncHttpTransport
6
+ from coffeemail.core.i18n import Locale
7
+ from coffeemail.core.types import ApiKeyIntrospection, CoffeeMailResponse
8
+ from coffeemail.resources.audiences import AsyncAudiences
9
+ from coffeemail.resources.broadcasts import AsyncBroadcasts
10
+ from coffeemail.resources.domains import AsyncDomains
11
+ from coffeemail.resources.emails import AsyncEmails
12
+ from coffeemail.resources.stats import AsyncStats
13
+ from coffeemail.resources.suppressions import AsyncSuppressions
14
+ from coffeemail.resources.templates import AsyncTemplates
15
+ from coffeemail.resources.webhooks import AsyncWebhooks
16
+
17
+
18
+ class AsyncCoffeeMail:
19
+ def __init__(
20
+ self,
21
+ api_key: str | None = None,
22
+ *,
23
+ base_url: str | None = None,
24
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
25
+ locale: Locale = "pt-BR",
26
+ http_client: httpx.AsyncClient | None = None,
27
+ ) -> None:
28
+ resolved_key = api_key or os.getenv("COFFEEMAIL_API_KEY")
29
+ self._transport = AsyncHttpTransport(
30
+ api_key=resolved_key,
31
+ base_url=base_url,
32
+ timeout=timeout,
33
+ locale=locale,
34
+ client=http_client,
35
+ )
36
+
37
+ self.emails = AsyncEmails(self._transport)
38
+ self.domains = AsyncDomains(self._transport)
39
+ self.templates = AsyncTemplates(self._transport)
40
+ self.audiences = AsyncAudiences(self._transport)
41
+ self.broadcasts = AsyncBroadcasts(self._transport)
42
+ self.suppressions = AsyncSuppressions(self._transport)
43
+ self.webhooks = AsyncWebhooks(self._transport)
44
+ self.stats = AsyncStats(self._transport)
45
+
46
+ async def introspect(
47
+ self, force_refresh: bool = False
48
+ ) -> CoffeeMailResponse[ApiKeyIntrospection]:
49
+ return await self._transport.introspect(force_refresh=force_refresh)
50
+
51
+ def invalidate_introspection_cache(self) -> None:
52
+ self._transport.invalidate_introspection_cache()
coffeemail/client.py ADDED
@@ -0,0 +1,50 @@
1
+ import os
2
+
3
+ import httpx
4
+
5
+ from coffeemail.core.http import DEFAULT_TIMEOUT_SECONDS, HttpTransport
6
+ from coffeemail.core.i18n import Locale
7
+ from coffeemail.core.types import ApiKeyIntrospection, CoffeeMailResponse
8
+ from coffeemail.resources.audiences import Audiences
9
+ from coffeemail.resources.broadcasts import Broadcasts
10
+ from coffeemail.resources.domains import Domains
11
+ from coffeemail.resources.emails import Emails
12
+ from coffeemail.resources.stats import Stats
13
+ from coffeemail.resources.suppressions import Suppressions
14
+ from coffeemail.resources.templates import Templates
15
+ from coffeemail.resources.webhooks import Webhooks
16
+
17
+
18
+ class CoffeeMail:
19
+ def __init__(
20
+ self,
21
+ api_key: str | None = None,
22
+ *,
23
+ base_url: str | None = None,
24
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
25
+ locale: Locale = "pt-BR",
26
+ http_client: httpx.Client | None = None,
27
+ ) -> None:
28
+ resolved_key = api_key or os.getenv("COFFEEMAIL_API_KEY")
29
+ self._transport = HttpTransport(
30
+ api_key=resolved_key,
31
+ base_url=base_url,
32
+ timeout=timeout,
33
+ locale=locale,
34
+ client=http_client,
35
+ )
36
+
37
+ self.emails = Emails(self._transport)
38
+ self.domains = Domains(self._transport)
39
+ self.templates = Templates(self._transport)
40
+ self.audiences = Audiences(self._transport)
41
+ self.broadcasts = Broadcasts(self._transport)
42
+ self.suppressions = Suppressions(self._transport)
43
+ self.webhooks = Webhooks(self._transport)
44
+ self.stats = Stats(self._transport)
45
+
46
+ def introspect(self, force_refresh: bool = False) -> CoffeeMailResponse[ApiKeyIntrospection]:
47
+ return self._transport.introspect(force_refresh=force_refresh)
48
+
49
+ def invalidate_introspection_cache(self) -> None:
50
+ self._transport.invalidate_introspection_cache()
@@ -0,0 +1,19 @@
1
+ import hashlib
2
+ import hmac
3
+
4
+
5
+ def verify_webhook_signature(
6
+ payload: str | bytes,
7
+ signature: str,
8
+ secret: str,
9
+ ) -> bool:
10
+ if not payload or not signature or not secret:
11
+ return False
12
+
13
+ try:
14
+ raw_bytes = payload.encode("utf-8") if isinstance(payload, str) else payload
15
+ secret_bytes = secret.encode("utf-8")
16
+ computed = hmac.new(secret_bytes, raw_bytes, hashlib.sha256).hexdigest()
17
+ return hmac.compare_digest(computed.lower(), signature.strip().lower())
18
+ except Exception:
19
+ return False
@@ -0,0 +1,88 @@
1
+ class CoffeeMailError(Exception):
2
+ def __init__(
3
+ self,
4
+ message: str,
5
+ status_code: int | None = None,
6
+ code: str | None = None,
7
+ details: dict[str, object] | None = None,
8
+ ) -> None:
9
+ super().__init__(message)
10
+ self.message = message
11
+ self.status_code = status_code
12
+ self.code = code
13
+ self.details = details or {}
14
+
15
+ def __str__(self) -> str:
16
+ if self.code:
17
+ return f"[{self.code}] {self.message}"
18
+ return self.message
19
+
20
+
21
+ class AuthenticationError(CoffeeMailError):
22
+ pass
23
+
24
+
25
+ class PermissionDeniedError(CoffeeMailError):
26
+ pass
27
+
28
+
29
+ class NotFoundError(CoffeeMailError):
30
+ pass
31
+
32
+
33
+ class ValidationError(CoffeeMailError):
34
+ pass
35
+
36
+
37
+ class ConflictError(CoffeeMailError):
38
+ pass
39
+
40
+
41
+ class RateLimitError(CoffeeMailError):
42
+ pass
43
+
44
+
45
+ class ServerError(CoffeeMailError):
46
+ pass
47
+
48
+
49
+ class NetworkError(CoffeeMailError):
50
+ pass
51
+
52
+
53
+ def create_error_from_response(
54
+ status_code: int,
55
+ body: dict[str, object] | None = None,
56
+ default_message: str | None = None,
57
+ ) -> CoffeeMailError:
58
+ data = body or {}
59
+ message = str(
60
+ data.get("message") or data.get("error") or default_message or f"HTTP Error {status_code}"
61
+ )
62
+ code = data.get("code")
63
+ str_code = str(code) if code else None
64
+ details = data.get("details")
65
+ details_dict = details if isinstance(details, dict) else {}
66
+
67
+ if status_code == 401:
68
+ return AuthenticationError(
69
+ message, status_code=status_code, code=str_code, details=details_dict
70
+ )
71
+ if status_code == 403:
72
+ return PermissionDeniedError(
73
+ message, status_code=status_code, code=str_code, details=details_dict
74
+ )
75
+ if status_code == 404:
76
+ return NotFoundError(message, status_code=status_code, code=str_code, details=details_dict)
77
+ if status_code in (400, 422):
78
+ return ValidationError(
79
+ message, status_code=status_code, code=str_code, details=details_dict
80
+ )
81
+ if status_code == 409:
82
+ return ConflictError(message, status_code=status_code, code=str_code, details=details_dict)
83
+ if status_code == 429:
84
+ return RateLimitError(message, status_code=status_code, code=str_code, details=details_dict)
85
+ if status_code >= 500:
86
+ return ServerError(message, status_code=status_code, code=str_code, details=details_dict)
87
+
88
+ return CoffeeMailError(message, status_code=status_code, code=str_code, details=details_dict)
@@ -0,0 +1,243 @@
1
+ import time
2
+ from collections.abc import Mapping
3
+ from typing import TypeVar
4
+
5
+ import httpx
6
+
7
+ from coffeemail.core.errors import (
8
+ AuthenticationError,
9
+ NetworkError,
10
+ create_error_from_response,
11
+ )
12
+ from coffeemail.core.i18n import Locale, get_message
13
+ from coffeemail.core.types import ApiKeyIntrospection, CoffeeMailResponse
14
+
15
+ SDK_VERSION = "0.1.0"
16
+ DEFAULT_BASE_URL = "https://api.coffeemail.com.br"
17
+ DEFAULT_TIMEOUT_SECONDS = 10.0
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ class BaseTransport:
23
+ def __init__(
24
+ self,
25
+ api_key: str | None = None,
26
+ base_url: str | None = None,
27
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
28
+ locale: Locale = "pt-BR",
29
+ ) -> None:
30
+ clean_key = (api_key or "").strip()
31
+ if not clean_key:
32
+ raise AuthenticationError(get_message("missing_api_key", locale))
33
+
34
+ self.api_key = clean_key
35
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
36
+ self.timeout = timeout
37
+ self.locale = locale
38
+
39
+ def get_headers(self, custom_headers: dict[str, str] | None = None) -> dict[str, str]:
40
+ headers = {
41
+ "Authorization": f"Bearer {self.api_key}",
42
+ "Content-Type": "application/json",
43
+ "Accept": "application/json",
44
+ "Accept-Language": self.locale,
45
+ "User-Agent": f"coffeemail-python/{SDK_VERSION}",
46
+ }
47
+ if custom_headers:
48
+ headers.update(custom_headers)
49
+ return headers
50
+
51
+
52
+ class HttpTransport(BaseTransport):
53
+ def __init__(
54
+ self,
55
+ api_key: str | None = None,
56
+ base_url: str | None = None,
57
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
58
+ locale: Locale = "pt-BR",
59
+ client: httpx.Client | None = None,
60
+ ) -> None:
61
+ super().__init__(api_key, base_url, timeout, locale)
62
+ self.client = client or httpx.Client(timeout=self.timeout)
63
+ self._cached_introspection: ApiKeyIntrospection | None = None
64
+ self._introspection_expires_at: float = 0.0
65
+
66
+ def request(
67
+ self,
68
+ method: str,
69
+ path: str,
70
+ params: Mapping[str, object] | None = None,
71
+ json_data: Mapping[str, object] | None = None,
72
+ headers: dict[str, str] | None = None,
73
+ ) -> CoffeeMailResponse[dict[str, object]]:
74
+ url = f"{self.base_url}/{path.lstrip('/')}"
75
+ req_headers = self.get_headers(headers)
76
+ clean_params = (
77
+ {k: str(v) for k, v in params.items() if v is not None} if params is not None else None
78
+ )
79
+
80
+ try:
81
+ response = self.client.request(
82
+ method=method,
83
+ url=url,
84
+ params=clean_params,
85
+ json=json_data,
86
+ headers=req_headers,
87
+ )
88
+ except httpx.TimeoutException as exc:
89
+ err = NetworkError(
90
+ get_message("timeout_error", self.locale), details={"original_error": str(exc)}
91
+ )
92
+ return CoffeeMailResponse(data=None, error=err, status_code=None)
93
+ except httpx.RequestError as exc:
94
+ err = NetworkError(
95
+ get_message("network_error", self.locale), details={"original_error": str(exc)}
96
+ )
97
+ return CoffeeMailResponse(data=None, error=err, status_code=None)
98
+
99
+ if not response.is_success:
100
+ body: dict[str, object] = {"message": response.text}
101
+ try:
102
+ parsed_body = response.json()
103
+ if isinstance(parsed_body, dict):
104
+ body = parsed_body
105
+ except Exception:
106
+ pass
107
+ error = create_error_from_response(response.status_code, body)
108
+ return CoffeeMailResponse(data=None, error=error, status_code=response.status_code)
109
+
110
+ data: dict[str, object] = {}
111
+ if response.content:
112
+ try:
113
+ parsed_data = response.json()
114
+ if isinstance(parsed_data, dict):
115
+ data = parsed_data
116
+ except Exception:
117
+ data = {}
118
+
119
+ return CoffeeMailResponse(data=data, error=None, status_code=response.status_code)
120
+
121
+ def introspect(self, force_refresh: bool = False) -> CoffeeMailResponse[ApiKeyIntrospection]:
122
+ now = time.time()
123
+ if (
124
+ not force_refresh
125
+ and self._cached_introspection
126
+ and now < self._introspection_expires_at
127
+ ):
128
+ return CoffeeMailResponse(data=self._cached_introspection, error=None, status_code=200)
129
+
130
+ raw_response = self.request("GET", "/v1/product/auth/me/api-key")
131
+ if raw_response.error or raw_response.data is None:
132
+ return CoffeeMailResponse(
133
+ data=None, error=raw_response.error, status_code=raw_response.status_code
134
+ )
135
+
136
+ introspection = ApiKeyIntrospection.model_validate(raw_response.data)
137
+ self._cached_introspection = introspection
138
+ self._introspection_expires_at = now + 300.0
139
+ return CoffeeMailResponse(
140
+ data=introspection, error=None, status_code=raw_response.status_code
141
+ )
142
+
143
+ def invalidate_introspection_cache(self) -> None:
144
+ self._cached_introspection = None
145
+ self._introspection_expires_at = 0.0
146
+
147
+
148
+ class AsyncHttpTransport(BaseTransport):
149
+ def __init__(
150
+ self,
151
+ api_key: str | None = None,
152
+ base_url: str | None = None,
153
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
154
+ locale: Locale = "pt-BR",
155
+ client: httpx.AsyncClient | None = None,
156
+ ) -> None:
157
+ super().__init__(api_key, base_url, timeout, locale)
158
+ self.client = client or httpx.AsyncClient(timeout=self.timeout)
159
+ self._cached_introspection: ApiKeyIntrospection | None = None
160
+ self._introspection_expires_at: float = 0.0
161
+
162
+ async def request(
163
+ self,
164
+ method: str,
165
+ path: str,
166
+ params: Mapping[str, object] | None = None,
167
+ json_data: Mapping[str, object] | None = None,
168
+ headers: dict[str, str] | None = None,
169
+ ) -> CoffeeMailResponse[dict[str, object]]:
170
+ url = f"{self.base_url}/{path.lstrip('/')}"
171
+ req_headers = self.get_headers(headers)
172
+ clean_params = (
173
+ {k: str(v) for k, v in params.items() if v is not None} if params is not None else None
174
+ )
175
+
176
+ try:
177
+ response = await self.client.request(
178
+ method=method,
179
+ url=url,
180
+ params=clean_params,
181
+ json=json_data,
182
+ headers=req_headers,
183
+ )
184
+ except httpx.TimeoutException as exc:
185
+ err = NetworkError(
186
+ get_message("timeout_error", self.locale), details={"original_error": str(exc)}
187
+ )
188
+ return CoffeeMailResponse(data=None, error=err, status_code=None)
189
+ except httpx.RequestError as exc:
190
+ err = NetworkError(
191
+ get_message("network_error", self.locale), details={"original_error": str(exc)}
192
+ )
193
+ return CoffeeMailResponse(data=None, error=err, status_code=None)
194
+
195
+ if not response.is_success:
196
+ body: dict[str, object] = {"message": response.text}
197
+ try:
198
+ parsed_body = response.json()
199
+ if isinstance(parsed_body, dict):
200
+ body = parsed_body
201
+ except Exception:
202
+ pass
203
+ error = create_error_from_response(response.status_code, body)
204
+ return CoffeeMailResponse(data=None, error=error, status_code=response.status_code)
205
+
206
+ data: dict[str, object] = {}
207
+ if response.content:
208
+ try:
209
+ parsed_data = response.json()
210
+ if isinstance(parsed_data, dict):
211
+ data = parsed_data
212
+ except Exception:
213
+ data = {}
214
+
215
+ return CoffeeMailResponse(data=data, error=None, status_code=response.status_code)
216
+
217
+ async def introspect(
218
+ self, force_refresh: bool = False
219
+ ) -> CoffeeMailResponse[ApiKeyIntrospection]:
220
+ now = time.time()
221
+ if (
222
+ not force_refresh
223
+ and self._cached_introspection
224
+ and now < self._introspection_expires_at
225
+ ):
226
+ return CoffeeMailResponse(data=self._cached_introspection, error=None, status_code=200)
227
+
228
+ raw_response = await self.request("GET", "/v1/product/auth/me/api-key")
229
+ if raw_response.error or raw_response.data is None:
230
+ return CoffeeMailResponse(
231
+ data=None, error=raw_response.error, status_code=raw_response.status_code
232
+ )
233
+
234
+ introspection = ApiKeyIntrospection.model_validate(raw_response.data)
235
+ self._cached_introspection = introspection
236
+ self._introspection_expires_at = now + 300.0
237
+ return CoffeeMailResponse(
238
+ data=introspection, error=None, status_code=raw_response.status_code
239
+ )
240
+
241
+ def invalidate_introspection_cache(self) -> None:
242
+ self._cached_introspection = None
243
+ self._introspection_expires_at = 0.0
@@ -0,0 +1,33 @@
1
+ from typing import Literal
2
+
3
+ Locale = Literal["pt-BR", "en"]
4
+
5
+ MESSAGES: dict[str, dict[Locale, str]] = {
6
+ "missing_api_key": {
7
+ "pt-BR": "Chave de API ausente. Forneça a chave no construtor ou defina a variável COFFEEMAIL_API_KEY.",
8
+ "en": "Missing API key. Pass it to the constructor or define COFFEEMAIL_API_KEY environment variable.",
9
+ },
10
+ "timeout_error": {
11
+ "pt-BR": "Tempo limite de conexão esgotado ao contatar o servidor da CoffeeMail.",
12
+ "en": "Connection timed out while contacting CoffeeMail server.",
13
+ },
14
+ "network_error": {
15
+ "pt-BR": "Falha na comunicação de rede com o servidor da CoffeeMail.",
16
+ "en": "Network communication failure with CoffeeMail server.",
17
+ },
18
+ "invalid_webhook_signature": {
19
+ "pt-BR": "Assinatura de webhook inválida ou corrompida.",
20
+ "en": "Invalid or corrupted webhook signature.",
21
+ },
22
+ "webhook_timestamp_expired": {
23
+ "pt-BR": "Timestamp do webhook expirou em relação à tolerância configurada.",
24
+ "en": "Webhook timestamp expired according to configured tolerance.",
25
+ },
26
+ }
27
+
28
+
29
+ def get_message(key: str, locale: Locale = "pt-BR") -> str:
30
+ entry = MESSAGES.get(key)
31
+ if not entry:
32
+ return key
33
+ return entry.get(locale, entry.get("pt-BR", key))
@@ -0,0 +1,50 @@
1
+ from collections.abc import Iterator
2
+ from typing import Generic, TypeVar
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+ from coffeemail.core.errors import CoffeeMailError
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class CoffeeMailResponse(Generic[T]):
12
+ def __init__(
13
+ self,
14
+ data: T | None = None,
15
+ error: CoffeeMailError | None = None,
16
+ status_code: int | None = None,
17
+ ) -> None:
18
+ self.data = data
19
+ self.error = error
20
+ self.status_code = status_code
21
+
22
+ @property
23
+ def is_success(self) -> bool:
24
+ return self.error is None
25
+
26
+ def unwrap(self) -> T:
27
+ if self.error is not None:
28
+ raise self.error
29
+ if self.data is None:
30
+ raise ValueError("Resposta sem dados disponíveis.")
31
+ return self.data
32
+
33
+ def __iter__(self) -> Iterator[object | None]:
34
+ yield self.data
35
+ yield self.error
36
+
37
+ def __repr__(self) -> str:
38
+ return f"CoffeeMailResponse(data={self.data!r}, error={self.error!r}, status_code={self.status_code})"
39
+
40
+
41
+ class ApiKeyIntrospection(BaseModel):
42
+ id: str
43
+ name: str
44
+ environment: str
45
+ scopes: list[str] = Field(default_factory=list)
46
+ rate_limit: dict[str, object] = Field(default_factory=dict, alias="rateLimit")
47
+ created_at: str = Field(alias="createdAt")
48
+ expires_at: str | None = Field(default=None, alias="expiresAt")
49
+
50
+ model_config = ConfigDict(populate_by_name=True)