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.
Files changed (69) hide show
  1. truagents-0.0.0.dev0/.gitignore +46 -0
  2. truagents-0.0.0.dev0/PKG-INFO +171 -0
  3. truagents-0.0.0.dev0/README.md +139 -0
  4. truagents-0.0.0.dev0/examples/oauth_flow.py +73 -0
  5. truagents-0.0.0.dev0/pyproject.toml +69 -0
  6. truagents-0.0.0.dev0/src/truagents/__init__.py +18 -0
  7. truagents-0.0.0.dev0/src/truagents/__version__.py +1 -0
  8. truagents-0.0.0.dev0/src/truagents/auth.py +293 -0
  9. truagents-0.0.0.dev0/src/truagents/client.py +304 -0
  10. truagents-0.0.0.dev0/src/truagents/errors.py +193 -0
  11. truagents-0.0.0.dev0/src/truagents/generated/__init__.py +8 -0
  12. truagents-0.0.0.dev0/src/truagents/generated/api/__init__.py +1 -0
  13. truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/__init__.py +1 -0
  14. truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/list_email_unsubscribes.py +290 -0
  15. truagents-0.0.0.dev0/src/truagents/generated/api/email_unsubscribe/push_email_unsubscribes.py +236 -0
  16. truagents-0.0.0.dev0/src/truagents/generated/api/o_auth/__init__.py +1 -0
  17. truagents-0.0.0.dev0/src/truagents/generated/api/o_auth/issue_o_auth_token.py +241 -0
  18. truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/__init__.py +1 -0
  19. truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/list_sms_unsubscribes.py +290 -0
  20. truagents-0.0.0.dev0/src/truagents/generated/api/sms_unsubscribe/push_sms_unsubscribes.py +232 -0
  21. truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/__init__.py +1 -0
  22. truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/list_phone_unsubscribes.py +298 -0
  23. truagents-0.0.0.dev0/src/truagents/generated/api/voice_unsubscribe/push_phone_unsubscribes.py +232 -0
  24. truagents-0.0.0.dev0/src/truagents/generated/client.py +282 -0
  25. truagents-0.0.0.dev0/src/truagents/generated/errors.py +16 -0
  26. truagents-0.0.0.dev0/src/truagents/generated/models/__init__.py +65 -0
  27. truagents-0.0.0.dev0/src/truagents/generated/models/email_skipped_item.py +83 -0
  28. truagents-0.0.0.dev0/src/truagents/generated/models/email_skipped_item_reason.py +11 -0
  29. truagents-0.0.0.dev0/src/truagents/generated/models/email_source_enum.py +11 -0
  30. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_batch_request.py +93 -0
  31. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_batch_response.py +119 -0
  32. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_item.py +50 -0
  33. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_list_response.py +113 -0
  34. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_record.py +98 -0
  35. truagents-0.0.0.dev0/src/truagents/generated/models/email_unsubscribe_updated_entry.py +79 -0
  36. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_client_credentials_request.py +87 -0
  37. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_client_credentials_request_grant_type.py +8 -0
  38. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_error_response.py +83 -0
  39. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_error_response_error.py +11 -0
  40. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_refresh_token_request.py +75 -0
  41. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_refresh_token_request_grant_type.py +8 -0
  42. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_token_response.py +97 -0
  43. truagents-0.0.0.dev0/src/truagents/generated/models/o_auth_token_response_token_type.py +8 -0
  44. truagents-0.0.0.dev0/src/truagents/generated/models/phone_skipped_item.py +83 -0
  45. truagents-0.0.0.dev0/src/truagents/generated/models/phone_skipped_item_reason.py +11 -0
  46. truagents-0.0.0.dev0/src/truagents/generated/models/phone_source_enum.py +11 -0
  47. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_batch_request.py +92 -0
  48. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_batch_response.py +118 -0
  49. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_item.py +51 -0
  50. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_list_response.py +111 -0
  51. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_record.py +99 -0
  52. truagents-0.0.0.dev0/src/truagents/generated/models/phone_unsubscribe_updated_entry.py +79 -0
  53. truagents-0.0.0.dev0/src/truagents/generated/models/rest_error_response.py +74 -0
  54. truagents-0.0.0.dev0/src/truagents/generated/models/unauthorized_organization_error.py +90 -0
  55. truagents-0.0.0.dev0/src/truagents/generated/models/unauthorized_organization_error_error.py +8 -0
  56. truagents-0.0.0.dev0/src/truagents/generated/types.py +54 -0
  57. truagents-0.0.0.dev0/src/truagents/observability.py +70 -0
  58. truagents-0.0.0.dev0/src/truagents/retry.py +64 -0
  59. truagents-0.0.0.dev0/tests/__init__.py +0 -0
  60. truagents-0.0.0.dev0/tests/conftest.py +1 -0
  61. truagents-0.0.0.dev0/tests/contract/__init__.py +0 -0
  62. truagents-0.0.0.dev0/tests/contract/conftest.py +31 -0
  63. truagents-0.0.0.dev0/tests/contract/test_oauth_positive_flow.py +35 -0
  64. truagents-0.0.0.dev0/tests/unit/__init__.py +0 -0
  65. truagents-0.0.0.dev0/tests/unit/test_auth.py +285 -0
  66. truagents-0.0.0.dev0/tests/unit/test_client.py +290 -0
  67. truagents-0.0.0.dev0/tests/unit/test_errors.py +223 -0
  68. truagents-0.0.0.dev0/tests/unit/test_observability.py +72 -0
  69. 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
+ [![PyPI version](https://img.shields.io/pypi/v/truagents.svg)](https://pypi.org/project/truagents/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/truagents.svg)](https://pypi.org/project/truagents/)
37
+ [![CI](https://github.com/ablt-ai/truagents-sdk/actions/workflows/contract-python.yml/badge.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/truagents.svg)](https://pypi.org/project/truagents/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/truagents.svg)](https://pypi.org/project/truagents/)
5
+ [![CI](https://github.com/ablt-ai/truagents-sdk/actions/workflows/contract-python.yml/badge.svg)](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"