truagents 0.0.0.dev0__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.
Files changed (56) hide show
  1. truagents/__init__.py +18 -0
  2. truagents/__version__.py +1 -0
  3. truagents/auth.py +293 -0
  4. truagents/client.py +304 -0
  5. truagents/errors.py +193 -0
  6. truagents/generated/__init__.py +8 -0
  7. truagents/generated/api/__init__.py +1 -0
  8. truagents/generated/api/email_unsubscribe/__init__.py +1 -0
  9. truagents/generated/api/email_unsubscribe/list_email_unsubscribes.py +290 -0
  10. truagents/generated/api/email_unsubscribe/push_email_unsubscribes.py +236 -0
  11. truagents/generated/api/o_auth/__init__.py +1 -0
  12. truagents/generated/api/o_auth/issue_o_auth_token.py +241 -0
  13. truagents/generated/api/sms_unsubscribe/__init__.py +1 -0
  14. truagents/generated/api/sms_unsubscribe/list_sms_unsubscribes.py +290 -0
  15. truagents/generated/api/sms_unsubscribe/push_sms_unsubscribes.py +232 -0
  16. truagents/generated/api/voice_unsubscribe/__init__.py +1 -0
  17. truagents/generated/api/voice_unsubscribe/list_phone_unsubscribes.py +298 -0
  18. truagents/generated/api/voice_unsubscribe/push_phone_unsubscribes.py +232 -0
  19. truagents/generated/client.py +282 -0
  20. truagents/generated/errors.py +16 -0
  21. truagents/generated/models/__init__.py +65 -0
  22. truagents/generated/models/email_skipped_item.py +83 -0
  23. truagents/generated/models/email_skipped_item_reason.py +11 -0
  24. truagents/generated/models/email_source_enum.py +11 -0
  25. truagents/generated/models/email_unsubscribe_batch_request.py +93 -0
  26. truagents/generated/models/email_unsubscribe_batch_response.py +119 -0
  27. truagents/generated/models/email_unsubscribe_item.py +50 -0
  28. truagents/generated/models/email_unsubscribe_list_response.py +113 -0
  29. truagents/generated/models/email_unsubscribe_record.py +98 -0
  30. truagents/generated/models/email_unsubscribe_updated_entry.py +79 -0
  31. truagents/generated/models/o_auth_client_credentials_request.py +87 -0
  32. truagents/generated/models/o_auth_client_credentials_request_grant_type.py +8 -0
  33. truagents/generated/models/o_auth_error_response.py +83 -0
  34. truagents/generated/models/o_auth_error_response_error.py +11 -0
  35. truagents/generated/models/o_auth_refresh_token_request.py +75 -0
  36. truagents/generated/models/o_auth_refresh_token_request_grant_type.py +8 -0
  37. truagents/generated/models/o_auth_token_response.py +97 -0
  38. truagents/generated/models/o_auth_token_response_token_type.py +8 -0
  39. truagents/generated/models/phone_skipped_item.py +83 -0
  40. truagents/generated/models/phone_skipped_item_reason.py +11 -0
  41. truagents/generated/models/phone_source_enum.py +11 -0
  42. truagents/generated/models/phone_unsubscribe_batch_request.py +92 -0
  43. truagents/generated/models/phone_unsubscribe_batch_response.py +118 -0
  44. truagents/generated/models/phone_unsubscribe_item.py +51 -0
  45. truagents/generated/models/phone_unsubscribe_list_response.py +111 -0
  46. truagents/generated/models/phone_unsubscribe_record.py +99 -0
  47. truagents/generated/models/phone_unsubscribe_updated_entry.py +79 -0
  48. truagents/generated/models/rest_error_response.py +74 -0
  49. truagents/generated/models/unauthorized_organization_error.py +90 -0
  50. truagents/generated/models/unauthorized_organization_error_error.py +8 -0
  51. truagents/generated/types.py +54 -0
  52. truagents/observability.py +70 -0
  53. truagents/retry.py +64 -0
  54. truagents-0.0.0.dev0.dist-info/METADATA +171 -0
  55. truagents-0.0.0.dev0.dist-info/RECORD +56 -0
  56. truagents-0.0.0.dev0.dist-info/WHEEL +4 -0
truagents/errors.py ADDED
@@ -0,0 +1,193 @@
1
+ """Typed exception hierarchy for the TruAgents SDK.
2
+
3
+ Two branches under a common `TruAgentsError` base:
4
+
5
+ - `AuthError` — RFC 6749-style OAuth failures raised by TokenManager.
6
+ - `APIError` — HTTP failures raised by resource methods on the client.
7
+
8
+ `NetworkError` wraps transport-layer exceptions (`httpx.TransportError` etc.).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import time
15
+ from datetime import UTC
16
+ from email.utils import parsedate_to_datetime
17
+ from typing import Any, Literal
18
+
19
+ import httpx
20
+
21
+
22
+ class TruAgentsError(Exception):
23
+ """Base class for all SDK errors.
24
+
25
+ Subclasses that add typed constructor arguments beyond the base class
26
+ must override `__reduce__` so `pickle.dumps` -> `pickle.loads` reconstructs
27
+ via those arguments. Without `__reduce__`, `Exception.args` carries only
28
+ the formatted message and unpickling calls the subclass with a single
29
+ positional string, dropping the typed fields (or raising `TypeError` for
30
+ multi-arg constructors). See `AuthError.__reduce__`, `APIError.__reduce__`,
31
+ `RateLimited.__reduce__`, and `NetworkError.__reduce__` for the pattern.
32
+ """
33
+
34
+ code: str = "TRUAGENTS_ERROR"
35
+
36
+
37
+ class AuthError(TruAgentsError):
38
+ """OAuth-token-endpoint failure. Mirrors RFC 6749 error responses."""
39
+
40
+ code = "AUTH_ERROR"
41
+
42
+ def __init__(self, http_status: int, error: str, error_description: str) -> None:
43
+ self.http_status = http_status
44
+ self.error = error
45
+ self.error_description = error_description
46
+ super().__init__(f"HTTP {http_status} error={error} error_description={error_description}")
47
+
48
+ def __reduce__(self) -> tuple[Any, tuple[Any, ...]]:
49
+ return (type(self), (self.http_status, self.error, self.error_description))
50
+
51
+
52
+ class InvalidClient(AuthError):
53
+ code = "INVALID_CLIENT"
54
+
55
+
56
+ class InvalidGrant(AuthError):
57
+ code = "INVALID_GRANT"
58
+
59
+
60
+ class TokenExpired(AuthError):
61
+ code = "TOKEN_EXPIRED"
62
+
63
+
64
+ class APIError(TruAgentsError):
65
+ """Non-2xx response from a resource endpoint."""
66
+
67
+ code = "API_ERROR"
68
+
69
+ def __init__(self, http_status: int, body: str) -> None:
70
+ self.http_status = http_status
71
+ self.body = body
72
+ super().__init__(f"HTTP {http_status} body={body}")
73
+
74
+ def __reduce__(self) -> tuple[Any, tuple[Any, ...]]:
75
+ return (type(self), (self.http_status, self.body))
76
+
77
+
78
+ class RateLimited(APIError):
79
+ code = "RATE_LIMITED"
80
+
81
+ def __init__(self, http_status: int, body: str, retry_after: float) -> None:
82
+ self.retry_after = retry_after
83
+ super().__init__(http_status, body)
84
+
85
+ def __str__(self) -> str:
86
+ return f"HTTP {self.http_status} body={self.body} retry_after={self.retry_after}s"
87
+
88
+ def __reduce__(self) -> tuple[Any, tuple[Any, ...]]:
89
+ return (type(self), (self.http_status, self.body, self.retry_after))
90
+
91
+
92
+ class NotFound(APIError):
93
+ code = "NOT_FOUND"
94
+
95
+
96
+ class InvalidRequest(APIError):
97
+ code = "INVALID_REQUEST"
98
+
99
+
100
+ class ServerError(APIError):
101
+ code = "SERVER_ERROR"
102
+
103
+
104
+ class NetworkError(TruAgentsError):
105
+ """Raised when the transport layer fails to complete a request.
106
+
107
+ `cause` carries the underlying `httpx` transport exception. That exception
108
+ may reference the originating request including its POST body (which for
109
+ the token endpoint contains `client_secret`). See `SECURITY.md` §
110
+ "Serialization of exceptions" before serializing a `NetworkError`.
111
+ """
112
+
113
+ code = "NETWORK_ERROR"
114
+
115
+ def __init__(self, cause: Exception) -> None:
116
+ self.cause = cause
117
+ super().__init__(f"Network error: {cause}")
118
+
119
+ def __reduce__(self) -> tuple[Any, tuple[Any, ...]]:
120
+ return (type(self), (self.cause,))
121
+
122
+
123
+ def _parse_retry_after(header_value: str | None) -> float:
124
+ """Parse the `Retry-After` header. Returns 0.0 when absent or unparseable.
125
+
126
+ Guards against `nan` / `inf` — a malformed 429 with `Retry-After: nan`
127
+ would otherwise flow into `time.sleep(nan)` and raise `ValueError` from
128
+ inside the retry loop as an untyped exception.
129
+ """
130
+ if not header_value:
131
+ return 0.0
132
+ try:
133
+ seconds = float(header_value)
134
+ except ValueError:
135
+ pass
136
+ else:
137
+ if not math.isfinite(seconds):
138
+ return 0.0
139
+ return seconds
140
+ try:
141
+ target = parsedate_to_datetime(header_value)
142
+ except (TypeError, ValueError):
143
+ return 0.0
144
+ if target is None:
145
+ return 0.0
146
+ now = time.time()
147
+ target_ts = target.astimezone(UTC).timestamp()
148
+ return max(target_ts - now, 0.0)
149
+
150
+
151
+ def _decode_body(response: httpx.Response) -> str:
152
+ try:
153
+ return response.text
154
+ except UnicodeDecodeError:
155
+ return response.content.decode(errors="replace")
156
+
157
+
158
+ def _extract_oauth_error_fields(response: httpx.Response) -> tuple[str, str]:
159
+ try:
160
+ body = response.json()
161
+ except ValueError:
162
+ return "unknown_error", ""
163
+ if not isinstance(body, dict):
164
+ return "unknown_error", ""
165
+ return (
166
+ str(body.get("error", "unknown_error")),
167
+ str(body.get("error_description", "")),
168
+ )
169
+
170
+
171
+ def classify_http_error(response: httpx.Response, kind: Literal["oauth", "api"]) -> TruAgentsError:
172
+ """Map an HTTP response to the appropriate SDK exception subclass."""
173
+ status = response.status_code
174
+ if kind == "oauth":
175
+ error, description = _extract_oauth_error_fields(response)
176
+ if status == 400 and error == "invalid_grant":
177
+ return InvalidGrant(status, error, description)
178
+ if status == 401:
179
+ return InvalidClient(status, error, description)
180
+ return AuthError(status, error, description)
181
+
182
+ body = _decode_body(response)
183
+ if status == 400:
184
+ return InvalidRequest(status, body)
185
+ if status == 401:
186
+ return TokenExpired(status, "token_expired", body)
187
+ if status == 404:
188
+ return NotFound(status, body)
189
+ if status == 429:
190
+ return RateLimited(status, body, _parse_retry_after(response.headers.get("Retry-After")))
191
+ if 500 <= status < 600:
192
+ return ServerError(status, body)
193
+ return APIError(status, body)
@@ -0,0 +1,8 @@
1
+ """A client library for accessing TruAgents Unsubscribe API"""
2
+
3
+ from .client import AuthenticatedClient, Client
4
+
5
+ __all__ = (
6
+ "AuthenticatedClient",
7
+ "Client",
8
+ )
@@ -0,0 +1 @@
1
+ """Contains methods for accessing the API"""
@@ -0,0 +1 @@
1
+ """Contains endpoint functions for accessing the API"""
@@ -0,0 +1,290 @@
1
+ import datetime
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from ... import errors
8
+ from ...client import AuthenticatedClient, Client
9
+ from ...models.email_unsubscribe_list_response import EmailUnsubscribeListResponse
10
+ from ...models.rest_error_response import RestErrorResponse
11
+ from ...models.unauthorized_organization_error import UnauthorizedOrganizationError
12
+ from ...types import UNSET, Response, Unset
13
+
14
+
15
+ def _get_kwargs(
16
+ *,
17
+ org_slug: str | Unset = UNSET,
18
+ since: datetime.datetime | Unset = UNSET,
19
+ until: datetime.datetime | Unset = UNSET,
20
+ cursor: str | Unset = UNSET,
21
+ limit: int | Unset = 100,
22
+ ) -> dict[str, Any]:
23
+
24
+ params: dict[str, Any] = {}
25
+
26
+ params["org_slug"] = org_slug
27
+
28
+ json_since: str | Unset = UNSET
29
+ if not isinstance(since, Unset):
30
+ json_since = since.isoformat()
31
+ params["since"] = json_since
32
+
33
+ json_until: str | Unset = UNSET
34
+ if not isinstance(until, Unset):
35
+ json_until = until.isoformat()
36
+ params["until"] = json_until
37
+
38
+ params["cursor"] = cursor
39
+
40
+ params["limit"] = limit
41
+
42
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
43
+
44
+ _kwargs: dict[str, Any] = {
45
+ "method": "get",
46
+ "url": "/api/v1/unsubscribe/email",
47
+ "params": params,
48
+ }
49
+
50
+ return _kwargs
51
+
52
+
53
+ def _parse_response(
54
+ *, client: AuthenticatedClient | Client, response: httpx.Response
55
+ ) -> (
56
+ EmailUnsubscribeListResponse
57
+ | RestErrorResponse
58
+ | UnauthorizedOrganizationError
59
+ | None
60
+ ):
61
+ if response.status_code == 200:
62
+ response_200 = EmailUnsubscribeListResponse.from_dict(response.json())
63
+
64
+ return response_200
65
+
66
+ if response.status_code == 400:
67
+ response_400 = RestErrorResponse.from_dict(response.json())
68
+
69
+ return response_400
70
+
71
+ if response.status_code == 401:
72
+ response_401 = RestErrorResponse.from_dict(response.json())
73
+
74
+ return response_401
75
+
76
+ if response.status_code == 403:
77
+ response_403 = UnauthorizedOrganizationError.from_dict(response.json())
78
+
79
+ return response_403
80
+
81
+ if response.status_code == 429:
82
+ response_429 = RestErrorResponse.from_dict(response.json())
83
+
84
+ return response_429
85
+
86
+ if client.raise_on_unexpected_status:
87
+ raise errors.UnexpectedStatus(response.status_code, response.content)
88
+ else:
89
+ return None
90
+
91
+
92
+ def _build_response(
93
+ *, client: AuthenticatedClient | Client, response: httpx.Response
94
+ ) -> Response[
95
+ EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError
96
+ ]:
97
+ return Response(
98
+ status_code=HTTPStatus(response.status_code),
99
+ content=response.content,
100
+ headers=response.headers,
101
+ parsed=_parse_response(client=client, response=response),
102
+ )
103
+
104
+
105
+ def sync_detailed(
106
+ *,
107
+ client: AuthenticatedClient | Client,
108
+ org_slug: str | Unset = UNSET,
109
+ since: datetime.datetime | Unset = UNSET,
110
+ until: datetime.datetime | Unset = UNSET,
111
+ cursor: str | Unset = UNSET,
112
+ limit: int | Unset = 100,
113
+ ) -> Response[
114
+ EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError
115
+ ]:
116
+ """List email opt-out / opt-in records
117
+
118
+ Returns the current state of email unsubscribe records for the organization addressed by `org_slug`
119
+ (or the `client_id`'s default organization when omitted), ordered by `updated_at` descending. Treat
120
+ the endpoint as a change stream: `since` / `until` bound the pagination window and `cursor` walks
121
+ toward older records. Rate limited to 60 requests per minute per `client_id`.
122
+
123
+ Args:
124
+ org_slug (str | Unset):
125
+ since (datetime.datetime | Unset):
126
+ until (datetime.datetime | Unset):
127
+ cursor (str | Unset):
128
+ limit (int | Unset): Default: 100.
129
+
130
+ Raises:
131
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
132
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
133
+
134
+ Returns:
135
+ Response[EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError]
136
+ """
137
+
138
+ kwargs = _get_kwargs(
139
+ org_slug=org_slug,
140
+ since=since,
141
+ until=until,
142
+ cursor=cursor,
143
+ limit=limit,
144
+ )
145
+
146
+ response = client.get_httpx_client().request(
147
+ **kwargs,
148
+ )
149
+
150
+ return _build_response(client=client, response=response)
151
+
152
+
153
+ def sync(
154
+ *,
155
+ client: AuthenticatedClient | Client,
156
+ org_slug: str | Unset = UNSET,
157
+ since: datetime.datetime | Unset = UNSET,
158
+ until: datetime.datetime | Unset = UNSET,
159
+ cursor: str | Unset = UNSET,
160
+ limit: int | Unset = 100,
161
+ ) -> (
162
+ EmailUnsubscribeListResponse
163
+ | RestErrorResponse
164
+ | UnauthorizedOrganizationError
165
+ | None
166
+ ):
167
+ """List email opt-out / opt-in records
168
+
169
+ Returns the current state of email unsubscribe records for the organization addressed by `org_slug`
170
+ (or the `client_id`'s default organization when omitted), ordered by `updated_at` descending. Treat
171
+ the endpoint as a change stream: `since` / `until` bound the pagination window and `cursor` walks
172
+ toward older records. Rate limited to 60 requests per minute per `client_id`.
173
+
174
+ Args:
175
+ org_slug (str | Unset):
176
+ since (datetime.datetime | Unset):
177
+ until (datetime.datetime | Unset):
178
+ cursor (str | Unset):
179
+ limit (int | Unset): Default: 100.
180
+
181
+ Raises:
182
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
183
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
184
+
185
+ Returns:
186
+ EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError
187
+ """
188
+
189
+ return sync_detailed(
190
+ client=client,
191
+ org_slug=org_slug,
192
+ since=since,
193
+ until=until,
194
+ cursor=cursor,
195
+ limit=limit,
196
+ ).parsed
197
+
198
+
199
+ async def asyncio_detailed(
200
+ *,
201
+ client: AuthenticatedClient | Client,
202
+ org_slug: str | Unset = UNSET,
203
+ since: datetime.datetime | Unset = UNSET,
204
+ until: datetime.datetime | Unset = UNSET,
205
+ cursor: str | Unset = UNSET,
206
+ limit: int | Unset = 100,
207
+ ) -> Response[
208
+ EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError
209
+ ]:
210
+ """List email opt-out / opt-in records
211
+
212
+ Returns the current state of email unsubscribe records for the organization addressed by `org_slug`
213
+ (or the `client_id`'s default organization when omitted), ordered by `updated_at` descending. Treat
214
+ the endpoint as a change stream: `since` / `until` bound the pagination window and `cursor` walks
215
+ toward older records. Rate limited to 60 requests per minute per `client_id`.
216
+
217
+ Args:
218
+ org_slug (str | Unset):
219
+ since (datetime.datetime | Unset):
220
+ until (datetime.datetime | Unset):
221
+ cursor (str | Unset):
222
+ limit (int | Unset): Default: 100.
223
+
224
+ Raises:
225
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
226
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
227
+
228
+ Returns:
229
+ Response[EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError]
230
+ """
231
+
232
+ kwargs = _get_kwargs(
233
+ org_slug=org_slug,
234
+ since=since,
235
+ until=until,
236
+ cursor=cursor,
237
+ limit=limit,
238
+ )
239
+
240
+ response = await client.get_async_httpx_client().request(**kwargs)
241
+
242
+ return _build_response(client=client, response=response)
243
+
244
+
245
+ async def asyncio(
246
+ *,
247
+ client: AuthenticatedClient | Client,
248
+ org_slug: str | Unset = UNSET,
249
+ since: datetime.datetime | Unset = UNSET,
250
+ until: datetime.datetime | Unset = UNSET,
251
+ cursor: str | Unset = UNSET,
252
+ limit: int | Unset = 100,
253
+ ) -> (
254
+ EmailUnsubscribeListResponse
255
+ | RestErrorResponse
256
+ | UnauthorizedOrganizationError
257
+ | None
258
+ ):
259
+ """List email opt-out / opt-in records
260
+
261
+ Returns the current state of email unsubscribe records for the organization addressed by `org_slug`
262
+ (or the `client_id`'s default organization when omitted), ordered by `updated_at` descending. Treat
263
+ the endpoint as a change stream: `since` / `until` bound the pagination window and `cursor` walks
264
+ toward older records. Rate limited to 60 requests per minute per `client_id`.
265
+
266
+ Args:
267
+ org_slug (str | Unset):
268
+ since (datetime.datetime | Unset):
269
+ until (datetime.datetime | Unset):
270
+ cursor (str | Unset):
271
+ limit (int | Unset): Default: 100.
272
+
273
+ Raises:
274
+ errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
275
+ httpx.TimeoutException: If the request takes longer than Client.timeout.
276
+
277
+ Returns:
278
+ EmailUnsubscribeListResponse | RestErrorResponse | UnauthorizedOrganizationError
279
+ """
280
+
281
+ return (
282
+ await asyncio_detailed(
283
+ client=client,
284
+ org_slug=org_slug,
285
+ since=since,
286
+ until=until,
287
+ cursor=cursor,
288
+ limit=limit,
289
+ )
290
+ ).parsed