truagents 0.0.0.dev0__tar.gz
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.
- truagents-0.0.0.dev0/.gitignore +46 -0
- truagents-0.0.0.dev0/PKG-INFO +171 -0
- truagents-0.0.0.dev0/README.md +139 -0
- truagents-0.0.0.dev0/examples/oauth_flow.py +73 -0
- truagents-0.0.0.dev0/pyproject.toml +69 -0
- truagents-0.0.0.dev0/src/truagents/__init__.py +18 -0
- truagents-0.0.0.dev0/src/truagents/__version__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/auth.py +293 -0
- truagents-0.0.0.dev0/src/truagents/client.py +304 -0
- truagents-0.0.0.dev0/src/truagents/errors.py +193 -0
- truagents-0.0.0.dev0/src/truagents/generated/__init__.py +8 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/__init__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/__init__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/list_email_unsubscribes.py +290 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/push_email_unsubscribes.py +236 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/o_auth/__init__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/o_auth/issue_o_auth_token.py +241 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/__init__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/list_sms_unsubscribes.py +290 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/push_sms_unsubscribes.py +232 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/__init__.py +1 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/list_phone_unsubscribes.py +298 -0
- truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/push_phone_unsubscribes.py +232 -0
- truagents-0.0.0.dev0/src/truagents/generated/client.py +282 -0
- truagents-0.0.0.dev0/src/truagents/generated/errors.py +16 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/__init__.py +65 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_skipped_item.py +83 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_skipped_item_reason.py +11 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_source_enum.py +11 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_batch_request.py +93 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_batch_response.py +119 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_item.py +50 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_list_response.py +113 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_record.py +98 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_updated_entry.py +79 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_client_credentials_request.py +87 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_client_credentials_request_grant_type.py +8 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_error_response.py +83 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_error_response_error.py +11 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_refresh_token_request.py +75 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_refresh_token_request_grant_type.py +8 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_token_response.py +97 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_token_response_token_type.py +8 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_skipped_item.py +83 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_skipped_item_reason.py +11 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_source_enum.py +11 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_batch_request.py +92 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_batch_response.py +118 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_item.py +51 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_list_response.py +111 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_record.py +99 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_updated_entry.py +79 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/rest_error_response.py +74 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/unauthorized_organization_error.py +90 -0
- truagents-0.0.0.dev0/src/truagents/generated/models/unauthorized_organization_error_error.py +8 -0
- truagents-0.0.0.dev0/src/truagents/generated/types.py +54 -0
- truagents-0.0.0.dev0/src/truagents/observability.py +70 -0
- truagents-0.0.0.dev0/src/truagents/retry.py +64 -0
- truagents-0.0.0.dev0/tests/__init__.py +0 -0
- truagents-0.0.0.dev0/tests/conftest.py +1 -0
- truagents-0.0.0.dev0/tests/contract/__init__.py +0 -0
- truagents-0.0.0.dev0/tests/contract/conftest.py +31 -0
- truagents-0.0.0.dev0/tests/contract/test_oauth_positive_flow.py +35 -0
- truagents-0.0.0.dev0/tests/unit/__init__.py +0 -0
- truagents-0.0.0.dev0/tests/unit/test_auth.py +285 -0
- truagents-0.0.0.dev0/tests/unit/test_client.py +290 -0
- truagents-0.0.0.dev0/tests/unit/test_errors.py +223 -0
- truagents-0.0.0.dev0/tests/unit/test_observability.py +72 -0
- truagents-0.0.0.dev0/tests/unit/test_retry.py +94 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.eggs/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
wheels/
|
|
11
|
+
*.whl
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Tooling caches
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.pyright/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.tox/
|
|
24
|
+
htmlcov/
|
|
25
|
+
.coverage
|
|
26
|
+
.coverage.*
|
|
27
|
+
coverage.xml
|
|
28
|
+
|
|
29
|
+
# Editors / IDEs
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
|
|
35
|
+
# OS junk
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# Secrets / credentials (never commit)
|
|
40
|
+
.env
|
|
41
|
+
.env.*
|
|
42
|
+
credentials*
|
|
43
|
+
*.pem
|
|
44
|
+
*.key
|
|
45
|
+
*.p12
|
|
46
|
+
*.pfx
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truagents
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Summary: Official Python SDK for the TruAgents Unsubscribe API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ablt-ai/truagents-sdk
|
|
6
|
+
Project-URL: Documentation, https://docs.truagents.com/sdks/python
|
|
7
|
+
Project-URL: Source, https://github.com/ablt-ai/truagents-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/ablt-ai/truagents-sdk/issues
|
|
9
|
+
Author: ablt-ai
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: oauth,sdk,truagents,unsubscribe
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: attrs>=25.0
|
|
22
|
+
Requires-Dist: httpx>=0.28
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pyright>=1.1.400; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff==0.15.18; extra == 'dev'
|
|
30
|
+
Requires-Dist: time-machine>=2.16; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# truagents — Python SDK for the TruAgents Unsubscribe API
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/truagents/)
|
|
36
|
+
[](https://pypi.org/project/truagents/)
|
|
37
|
+
[](https://github.com/ablt-ai/truagents-sdk/actions/workflows/contract-python.yml)
|
|
38
|
+
|
|
39
|
+
Official Python SDK for the [TruAgents Unsubscribe API](https://docs.truagents.com).
|
|
40
|
+
Handles OAuth token acquisition and refresh, retries, rate limiting, and
|
|
41
|
+
observability so you can focus on the business logic.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install truagents
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python `>= 3.11`.
|
|
50
|
+
|
|
51
|
+
## Quickstart (sync)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from truagents import Client
|
|
55
|
+
|
|
56
|
+
with Client(client_id="...", client_secret="...") as client:
|
|
57
|
+
resp = client.list_email_unsubscribes()
|
|
58
|
+
for record in resp.data:
|
|
59
|
+
print(record.email, record.unsubscribed)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quickstart (async)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
|
|
67
|
+
from truagents import AsyncClient
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def main() -> None:
|
|
71
|
+
async with AsyncClient(client_id="...", client_secret="...") as client:
|
|
72
|
+
resp = await client.list_email_unsubscribes()
|
|
73
|
+
for record in resp.data:
|
|
74
|
+
print(record.email, record.unsubscribed)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Authentication
|
|
81
|
+
|
|
82
|
+
`TokenManager` (owned by the client) mints an access token via
|
|
83
|
+
`client_credentials` on the first request and refreshes it via `refresh_token`
|
|
84
|
+
`refresh_threshold_seconds` before the server-side expiry (default: 60s).
|
|
85
|
+
Concurrent callers are de-duplicated: a thundering herd of sync threads or
|
|
86
|
+
async coroutines results in a single `/oauth/token` round-trip.
|
|
87
|
+
|
|
88
|
+
If the cached refresh token is rejected with `invalid_grant` (RFC 6749 §5.2
|
|
89
|
+
family revoke), the manager transparently drops it and re-mints via
|
|
90
|
+
`client_credentials`. Partners never see the intermediate `InvalidGrant`.
|
|
91
|
+
|
|
92
|
+
**A mid-flight `TokenExpired` (HTTP 401) on an API endpoint is NOT
|
|
93
|
+
auto-remedied.** It signals that your credentials were revoked externally and
|
|
94
|
+
you need to investigate. This is a deliberate design decision — silently
|
|
95
|
+
re-minting on 401 would mask real revocations.
|
|
96
|
+
|
|
97
|
+
## Retries + rate limiting
|
|
98
|
+
|
|
99
|
+
Every request goes through a shared `RetryPolicy`:
|
|
100
|
+
|
|
101
|
+
| Failure | Retry? |
|
|
102
|
+
|----------------------------------|-------------------------------------------|
|
|
103
|
+
| `NetworkError` | Yes — up to `max_attempts` with exp. back-off |
|
|
104
|
+
| `ServerError` (5xx) | Yes — up to `max_attempts` with exp. back-off |
|
|
105
|
+
| `RateLimited` (HTTP 429) | Yes — honours `Retry-After` (capped at 60s) |
|
|
106
|
+
| `InvalidRequest` (HTTP 400) | No |
|
|
107
|
+
| `NotFound` (HTTP 404) | No |
|
|
108
|
+
| `TokenExpired` (HTTP 401) | No |
|
|
109
|
+
|
|
110
|
+
Override defaults by constructing your own `RetryPolicy`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from truagents import Client, RetryPolicy
|
|
114
|
+
|
|
115
|
+
policy = RetryPolicy(max_attempts=5, base_delay=0.1, retry_after_cap=30.0)
|
|
116
|
+
client = Client(client_id="...", client_secret="...", retry=policy)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Observability hooks
|
|
120
|
+
|
|
121
|
+
`Hooks` fire around every HTTP call (token endpoint and API endpoint alike).
|
|
122
|
+
Broken hooks never break an SDK call — they are logged and swallowed.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from truagents import Client, Hooks, Request, Response
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def log_request(req: Request) -> None:
|
|
129
|
+
print(f"-> {req.method} {req.url}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def log_response(resp: Response, elapsed_ms: float) -> None:
|
|
133
|
+
print(f"<- {resp.status_code} ({elapsed_ms:.0f}ms)")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
hooks = Hooks(on_request=log_request, on_response=log_response)
|
|
137
|
+
client = Client(client_id="...", client_secret="...", hooks=hooks)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Error handling
|
|
141
|
+
|
|
142
|
+
All SDK exceptions inherit from `truagents.errors.TruAgentsError`.
|
|
143
|
+
|
|
144
|
+
| Exception | Raised when |
|
|
145
|
+
|----------------------|----------------------------------------------------|
|
|
146
|
+
| `AuthError` | Base of OAuth token endpoint failures. |
|
|
147
|
+
| `InvalidClient` | Token endpoint returned 401 (bad client_id/secret). |
|
|
148
|
+
| `InvalidGrant` | Token endpoint returned 400 `invalid_grant`. Almost always caught + recovered internally. |
|
|
149
|
+
| `TokenExpired` | API endpoint returned 401 (external revocation). |
|
|
150
|
+
| `APIError` | Base of API endpoint failures. |
|
|
151
|
+
| `InvalidRequest` | API endpoint returned 400. |
|
|
152
|
+
| `NotFound` | API endpoint returned 404. |
|
|
153
|
+
| `RateLimited` | API endpoint returned 429. Carries `retry_after`. |
|
|
154
|
+
| `ServerError` | API endpoint returned 5xx. |
|
|
155
|
+
| `NetworkError` | Transport failure (DNS, connection reset, timeout). Wraps the underlying `httpx` exception. |
|
|
156
|
+
|
|
157
|
+
Every subclass carries a `code` class variable (e.g. `"INVALID_CLIENT"`) so
|
|
158
|
+
you can log it without importing the class.
|
|
159
|
+
|
|
160
|
+
## Version stability
|
|
161
|
+
|
|
162
|
+
The SDK is at `0.x`. Minor bumps may introduce breaking changes. See
|
|
163
|
+
[`docs/versioning.md`](../../docs/versioning.md) at the repository root for
|
|
164
|
+
the full policy and the `1.0.0` promotion criteria.
|
|
165
|
+
|
|
166
|
+
## Links
|
|
167
|
+
|
|
168
|
+
- Homepage: <https://github.com/ablt-ai/truagents-sdk>
|
|
169
|
+
- Docs: <https://docs.truagents.com>
|
|
170
|
+
- Issues: <https://github.com/ablt-ai/truagents-sdk/issues>
|
|
171
|
+
- License: Apache 2.0 (see [`LICENSE`](../../LICENSE))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# truagents — Python SDK for the TruAgents Unsubscribe API
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/truagents/)
|
|
4
|
+
[](https://pypi.org/project/truagents/)
|
|
5
|
+
[](https://github.com/ablt-ai/truagents-sdk/actions/workflows/contract-python.yml)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for the [TruAgents Unsubscribe API](https://docs.truagents.com).
|
|
8
|
+
Handles OAuth token acquisition and refresh, retries, rate limiting, and
|
|
9
|
+
observability so you can focus on the business logic.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install truagents
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python `>= 3.11`.
|
|
18
|
+
|
|
19
|
+
## Quickstart (sync)
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from truagents import Client
|
|
23
|
+
|
|
24
|
+
with Client(client_id="...", client_secret="...") as client:
|
|
25
|
+
resp = client.list_email_unsubscribes()
|
|
26
|
+
for record in resp.data:
|
|
27
|
+
print(record.email, record.unsubscribed)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart (async)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
|
|
35
|
+
from truagents import AsyncClient
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def main() -> None:
|
|
39
|
+
async with AsyncClient(client_id="...", client_secret="...") as client:
|
|
40
|
+
resp = await client.list_email_unsubscribes()
|
|
41
|
+
for record in resp.data:
|
|
42
|
+
print(record.email, record.unsubscribed)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
`TokenManager` (owned by the client) mints an access token via
|
|
51
|
+
`client_credentials` on the first request and refreshes it via `refresh_token`
|
|
52
|
+
`refresh_threshold_seconds` before the server-side expiry (default: 60s).
|
|
53
|
+
Concurrent callers are de-duplicated: a thundering herd of sync threads or
|
|
54
|
+
async coroutines results in a single `/oauth/token` round-trip.
|
|
55
|
+
|
|
56
|
+
If the cached refresh token is rejected with `invalid_grant` (RFC 6749 §5.2
|
|
57
|
+
family revoke), the manager transparently drops it and re-mints via
|
|
58
|
+
`client_credentials`. Partners never see the intermediate `InvalidGrant`.
|
|
59
|
+
|
|
60
|
+
**A mid-flight `TokenExpired` (HTTP 401) on an API endpoint is NOT
|
|
61
|
+
auto-remedied.** It signals that your credentials were revoked externally and
|
|
62
|
+
you need to investigate. This is a deliberate design decision — silently
|
|
63
|
+
re-minting on 401 would mask real revocations.
|
|
64
|
+
|
|
65
|
+
## Retries + rate limiting
|
|
66
|
+
|
|
67
|
+
Every request goes through a shared `RetryPolicy`:
|
|
68
|
+
|
|
69
|
+
| Failure | Retry? |
|
|
70
|
+
|----------------------------------|-------------------------------------------|
|
|
71
|
+
| `NetworkError` | Yes — up to `max_attempts` with exp. back-off |
|
|
72
|
+
| `ServerError` (5xx) | Yes — up to `max_attempts` with exp. back-off |
|
|
73
|
+
| `RateLimited` (HTTP 429) | Yes — honours `Retry-After` (capped at 60s) |
|
|
74
|
+
| `InvalidRequest` (HTTP 400) | No |
|
|
75
|
+
| `NotFound` (HTTP 404) | No |
|
|
76
|
+
| `TokenExpired` (HTTP 401) | No |
|
|
77
|
+
|
|
78
|
+
Override defaults by constructing your own `RetryPolicy`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from truagents import Client, RetryPolicy
|
|
82
|
+
|
|
83
|
+
policy = RetryPolicy(max_attempts=5, base_delay=0.1, retry_after_cap=30.0)
|
|
84
|
+
client = Client(client_id="...", client_secret="...", retry=policy)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Observability hooks
|
|
88
|
+
|
|
89
|
+
`Hooks` fire around every HTTP call (token endpoint and API endpoint alike).
|
|
90
|
+
Broken hooks never break an SDK call — they are logged and swallowed.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from truagents import Client, Hooks, Request, Response
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def log_request(req: Request) -> None:
|
|
97
|
+
print(f"-> {req.method} {req.url}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def log_response(resp: Response, elapsed_ms: float) -> None:
|
|
101
|
+
print(f"<- {resp.status_code} ({elapsed_ms:.0f}ms)")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
hooks = Hooks(on_request=log_request, on_response=log_response)
|
|
105
|
+
client = Client(client_id="...", client_secret="...", hooks=hooks)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Error handling
|
|
109
|
+
|
|
110
|
+
All SDK exceptions inherit from `truagents.errors.TruAgentsError`.
|
|
111
|
+
|
|
112
|
+
| Exception | Raised when |
|
|
113
|
+
|----------------------|----------------------------------------------------|
|
|
114
|
+
| `AuthError` | Base of OAuth token endpoint failures. |
|
|
115
|
+
| `InvalidClient` | Token endpoint returned 401 (bad client_id/secret). |
|
|
116
|
+
| `InvalidGrant` | Token endpoint returned 400 `invalid_grant`. Almost always caught + recovered internally. |
|
|
117
|
+
| `TokenExpired` | API endpoint returned 401 (external revocation). |
|
|
118
|
+
| `APIError` | Base of API endpoint failures. |
|
|
119
|
+
| `InvalidRequest` | API endpoint returned 400. |
|
|
120
|
+
| `NotFound` | API endpoint returned 404. |
|
|
121
|
+
| `RateLimited` | API endpoint returned 429. Carries `retry_after`. |
|
|
122
|
+
| `ServerError` | API endpoint returned 5xx. |
|
|
123
|
+
| `NetworkError` | Transport failure (DNS, connection reset, timeout). Wraps the underlying `httpx` exception. |
|
|
124
|
+
|
|
125
|
+
Every subclass carries a `code` class variable (e.g. `"INVALID_CLIENT"`) so
|
|
126
|
+
you can log it without importing the class.
|
|
127
|
+
|
|
128
|
+
## Version stability
|
|
129
|
+
|
|
130
|
+
The SDK is at `0.x`. Minor bumps may introduce breaking changes. See
|
|
131
|
+
[`docs/versioning.md`](../../docs/versioning.md) at the repository root for
|
|
132
|
+
the full policy and the `1.0.0` promotion criteria.
|
|
133
|
+
|
|
134
|
+
## Links
|
|
135
|
+
|
|
136
|
+
- Homepage: <https://github.com/ablt-ai/truagents-sdk>
|
|
137
|
+
- Docs: <https://docs.truagents.com>
|
|
138
|
+
- Issues: <https://github.com/ablt-ai/truagents-sdk/issues>
|
|
139
|
+
- License: Apache 2.0 (see [`LICENSE`](../../LICENSE))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Runnable positive-flow demo of the TruAgents Python SDK.
|
|
2
|
+
|
|
3
|
+
Steps:
|
|
4
|
+
1. Read CLIENT_ID / CLIENT_SECRET / BASE_URL from env.
|
|
5
|
+
2. Mint an access token via `client_credentials` (via TokenManager).
|
|
6
|
+
3. GET /api/v1/unsubscribe/email with the fresh token.
|
|
7
|
+
4. Force a rotation by expiring the cache and re-fetching the token.
|
|
8
|
+
5. GET /api/v1/unsubscribe/email with the rotated token.
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 — every step OK.
|
|
12
|
+
1 — unexpected outcome.
|
|
13
|
+
2 — env missing.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
export CLIENT_ID=...
|
|
17
|
+
export CLIENT_SECRET=...
|
|
18
|
+
python examples/oauth_flow.py
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
from truagents import Client, errors
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.dev-truagents.com"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_env() -> tuple[str, str, str]:
|
|
32
|
+
client_id = os.environ.get("CLIENT_ID")
|
|
33
|
+
client_secret = os.environ.get("CLIENT_SECRET")
|
|
34
|
+
base_url = os.environ.get("BASE_URL", DEFAULT_BASE_URL)
|
|
35
|
+
if not client_id or not client_secret:
|
|
36
|
+
print("Missing CLIENT_ID / CLIENT_SECRET", file=sys.stderr)
|
|
37
|
+
sys.exit(2)
|
|
38
|
+
return client_id, client_secret, base_url
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prefix(value: str) -> str:
|
|
42
|
+
return f"{value[:12]}..." if value else "<none>"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> int:
|
|
46
|
+
client_id, client_secret, base_url = read_env()
|
|
47
|
+
print(f"Step 1: env resolved — CLIENT_ID={_prefix(client_id)} BASE_URL={base_url}")
|
|
48
|
+
|
|
49
|
+
with Client(client_id=client_id, client_secret=client_secret, base_url=base_url) as client:
|
|
50
|
+
try:
|
|
51
|
+
step2_token = client._token_manager.get_access_token()
|
|
52
|
+
print(f"Step 2: mint OK — access_token={_prefix(step2_token)}")
|
|
53
|
+
|
|
54
|
+
step3 = client.list_email_unsubscribes()
|
|
55
|
+
print(f"Step 3: GET /api/v1/unsubscribe/email OK — items_count={len(step3.data)}")
|
|
56
|
+
|
|
57
|
+
# test/demo escape hatch — not part of public SDK API; do not copy into production
|
|
58
|
+
client._token_manager._expire_now()
|
|
59
|
+
step4_token = client._token_manager.get_access_token()
|
|
60
|
+
print(f"Step 4: refresh rotation OK — new_token={_prefix(step4_token)}")
|
|
61
|
+
|
|
62
|
+
step5 = client.list_email_unsubscribes()
|
|
63
|
+
print(f"Step 5: GET with rotated token OK — items_count={len(step5.data)}")
|
|
64
|
+
except errors.TruAgentsError as err:
|
|
65
|
+
print(f"Unexpected error: {err.code} {err}", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
print("All 5 positive-flow steps completed as expected.")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "truagents"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python SDK for the TruAgents Unsubscribe API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "ablt-ai" }]
|
|
13
|
+
keywords = ["truagents", "unsubscribe", "sdk", "oauth"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.28",
|
|
26
|
+
"attrs>=25.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-asyncio>=0.24",
|
|
33
|
+
"pytest-timeout>=2.3",
|
|
34
|
+
"respx>=0.22",
|
|
35
|
+
"time-machine>=2.16",
|
|
36
|
+
"ruff==0.15.18",
|
|
37
|
+
"pyright>=1.1.400",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/ablt-ai/truagents-sdk"
|
|
42
|
+
Documentation = "https://docs.truagents.com/sdks/python"
|
|
43
|
+
Source = "https://github.com/ablt-ai/truagents-sdk"
|
|
44
|
+
Issues = "https://github.com/ablt-ai/truagents-sdk/issues"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/truagents/__version__.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/truagents"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 120
|
|
54
|
+
target-version = "py312"
|
|
55
|
+
extend-exclude = ["src/truagents/generated"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "W", "B", "UP"]
|
|
59
|
+
|
|
60
|
+
[tool.pyright]
|
|
61
|
+
include = ["src/truagents"]
|
|
62
|
+
exclude = ["tests/**", "src/truagents/generated"]
|
|
63
|
+
pythonVersion = "3.12"
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
asyncio_mode = "auto"
|
|
67
|
+
timeout = 300
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
addopts = "-vvv -rsfEx --tb=short"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from truagents import errors
|
|
2
|
+
from truagents.__version__ import __version__
|
|
3
|
+
from truagents.auth import TokenManager
|
|
4
|
+
from truagents.client import AsyncClient, Client
|
|
5
|
+
from truagents.observability import Hooks, Request, Response
|
|
6
|
+
from truagents.retry import RetryPolicy
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AsyncClient",
|
|
10
|
+
"Client",
|
|
11
|
+
"Hooks",
|
|
12
|
+
"Request",
|
|
13
|
+
"Response",
|
|
14
|
+
"RetryPolicy",
|
|
15
|
+
"TokenManager",
|
|
16
|
+
"__version__",
|
|
17
|
+
"errors",
|
|
18
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.0.dev0"
|