authpi-admin 0.3.0__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.
- authpi_admin-0.3.0/.gitignore +51 -0
- authpi_admin-0.3.0/CHANGELOG.md +25 -0
- authpi_admin-0.3.0/PKG-INFO +285 -0
- authpi_admin-0.3.0/README.md +253 -0
- authpi_admin-0.3.0/authpi_admin/__init__.py +55 -0
- authpi_admin-0.3.0/authpi_admin/_version.py +5 -0
- authpi_admin-0.3.0/authpi_admin/client.py +155 -0
- authpi_admin-0.3.0/authpi_admin/errors.py +218 -0
- authpi_admin-0.3.0/authpi_admin/generated/__init__.py +28 -0
- authpi_admin-0.3.0/authpi_admin/generated/models.py +380 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/__init__.py +0 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/accounts.py +157 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/agents.py +177 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/api_keys.py +213 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/approvals.py +109 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/auth_methods.py +148 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/clients.py +197 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/deliveries.py +83 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/domains.py +194 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/events.py +111 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/groups.py +160 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/invitations.py +202 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/issuers.py +157 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/members.py +154 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/notes.py +192 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/organizations.py +244 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/sessions.py +125 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/tokens.py +94 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/trusted_devices.py +102 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/users.py +224 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/verifiers.py +102 -0
- authpi_admin-0.3.0/authpi_admin/generated/resources/webhooks.py +167 -0
- authpi_admin-0.3.0/authpi_admin/generated/scopes/__init__.py +0 -0
- authpi_admin-0.3.0/authpi_admin/generated/scopes/agent_scope.py +84 -0
- authpi_admin-0.3.0/authpi_admin/generated/scopes/issuer_scope.py +160 -0
- authpi_admin-0.3.0/authpi_admin/generated/scopes/user_scope.py +102 -0
- authpi_admin-0.3.0/authpi_admin/generated/scopes/webhook_scope.py +84 -0
- authpi_admin-0.3.0/authpi_admin/http_client.py +305 -0
- authpi_admin-0.3.0/authpi_admin/pagination.py +41 -0
- authpi_admin-0.3.0/authpi_admin/py.typed +0 -0
- authpi_admin-0.3.0/authpi_admin/user_agent.py +21 -0
- authpi_admin-0.3.0/pyproject.toml +69 -0
- authpi_admin-0.3.0/tests/__init__.py +0 -0
- authpi_admin-0.3.0/tests/test_client.py +90 -0
- authpi_admin-0.3.0/tests/test_errors.py +193 -0
- authpi_admin-0.3.0/tests/test_http_client.py +461 -0
- authpi_admin-0.3.0/tests/test_integration.py +281 -0
- authpi_admin-0.3.0/tests/test_pagination.py +74 -0
- authpi_admin-0.3.0/uv.lock +483 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
node_modules
|
|
3
|
+
/build
|
|
4
|
+
.svelte-kit
|
|
5
|
+
/package
|
|
6
|
+
.wrangler
|
|
7
|
+
.env
|
|
8
|
+
.env.*
|
|
9
|
+
.dev.vars
|
|
10
|
+
!.env.example
|
|
11
|
+
!.env.e2e.example
|
|
12
|
+
vite.config.js.timestamp-*
|
|
13
|
+
vite.config.ts.timestamp-*
|
|
14
|
+
oracle.sql
|
|
15
|
+
dist
|
|
16
|
+
storybook-static
|
|
17
|
+
target
|
|
18
|
+
filter/
|
|
19
|
+
Cargo.lock
|
|
20
|
+
/sdk/core/
|
|
21
|
+
/sdk/oidc/
|
|
22
|
+
.stoplight
|
|
23
|
+
ratelimiter/
|
|
24
|
+
reproDo/
|
|
25
|
+
__pycache__/
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
notebooks/
|
|
28
|
+
test-adyen/
|
|
29
|
+
stripe-authpi/
|
|
30
|
+
stripe-app/
|
|
31
|
+
secrets.json
|
|
32
|
+
.idea
|
|
33
|
+
.vscode
|
|
34
|
+
*.pem
|
|
35
|
+
*.key
|
|
36
|
+
*.crt
|
|
37
|
+
*.csr
|
|
38
|
+
*.env
|
|
39
|
+
output.txt
|
|
40
|
+
output.yaml
|
|
41
|
+
pnpm-lock.yaml
|
|
42
|
+
worker-configuration.d.ts
|
|
43
|
+
|
|
44
|
+
# Claude Code ephemeral artifacts
|
|
45
|
+
.claude/design-explorations/
|
|
46
|
+
.claude/plans/
|
|
47
|
+
.claude/worktrees/
|
|
48
|
+
.superpowers/
|
|
49
|
+
.worktrees/
|
|
50
|
+
docs/plans/
|
|
51
|
+
docs/superpowers/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/arbfay/authpi/compare/authpi-admin-v0.2.0...authpi-admin-v0.3.0) (2026-04-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **sdks:** set custom User-Agent for SDK identification ([#213](https://github.com/arbfay/authpi/issues/213)) ([dec716c](https://github.com/arbfay/authpi/commit/dec716c99d877e99496f45c69f3625341bb2bc2e))
|
|
9
|
+
|
|
10
|
+
## [0.2.0](https://github.com/arbfay/authpi/compare/authpi-admin-v0.1.0...authpi-admin-v0.2.0) (2026-04-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* All generated type interfaces now use snake_case field names. Consumers referencing camelCase fields (e.g., Client.issuerId) must update to snake_case (Client.issuer_id).
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* Admin SDK code generator + Python SDK (authpi-admin) ([#186](https://github.com/arbfay/authpi/issues/186)) ([3eb5055](https://github.com/arbfay/authpi/commit/3eb505579f34a49769e0b01eb59f6d571ad74571))
|
|
20
|
+
* SDK convergence — extend @authpi/admin, migrate console, security fixes ([#197](https://github.com/arbfay/authpi/issues/197)) ([3d4896f](https://github.com/arbfay/authpi/commit/3d4896fa60381542d99a66e394809d128bf6bcf0))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **admin:** fix custom action paths, security hardening for TS and Python SDKs ([#189](https://github.com/arbfay/authpi/issues/189)) ([bf5e0b4](https://github.com/arbfay/authpi/commit/bf5e0b430a76173a1a1b4ff188095a96b337a74c))
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authpi-admin
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official Python Admin SDK for AuthPI Core API
|
|
5
|
+
Project-URL: Homepage, https://authpi.com
|
|
6
|
+
Project-URL: Documentation, https://docs.authpi.com/sdk/python/admin
|
|
7
|
+
Project-URL: Repository, https://github.com/arbfay/authpi
|
|
8
|
+
Author-email: AuthPI <hello@authpi.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: admin,api,authentication,authorization,authpi,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT 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: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.13.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-httpx>=0.34.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# authpi-admin
|
|
34
|
+
|
|
35
|
+
Official Python Admin SDK for the AuthPI Core API.
|
|
36
|
+
|
|
37
|
+
**Requirements:** Python 3.11+, async-only (`httpx` + `asyncio`)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install authpi-admin
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from authpi_admin import AuthPIAdmin
|
|
49
|
+
|
|
50
|
+
async with AuthPIAdmin(api_key="key_xxx") as admin:
|
|
51
|
+
# List issuers
|
|
52
|
+
page = await admin.issuers.list(limit=10)
|
|
53
|
+
print(page.data)
|
|
54
|
+
|
|
55
|
+
# Scope into an issuer and manage users
|
|
56
|
+
users = await admin.issuer("iss_xxx").users.list()
|
|
57
|
+
|
|
58
|
+
# Auto-paginate
|
|
59
|
+
async for user in admin.issuer("iss_xxx").users.list_all():
|
|
60
|
+
print(user)
|
|
61
|
+
|
|
62
|
+
# Create a user
|
|
63
|
+
user = await admin.issuer("iss_xxx").users.create({
|
|
64
|
+
"email": "alice@example.com",
|
|
65
|
+
"display_name": "Alice",
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
### API Key (default)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Bearer Token
|
|
78
|
+
|
|
79
|
+
For server-side applications authenticating on behalf of a user session:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
admin = AuthPIAdmin(
|
|
83
|
+
access_token="tok_xxx",
|
|
84
|
+
account_id="acc_xxx",
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
With optional token refresh callback:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async def refresh():
|
|
92
|
+
new_tokens = await my_refresh_logic()
|
|
93
|
+
return {"access_token": new_tokens.access_token}
|
|
94
|
+
|
|
95
|
+
admin = AuthPIAdmin(
|
|
96
|
+
access_token="tok_xxx",
|
|
97
|
+
account_id="acc_xxx",
|
|
98
|
+
on_token_expired=refresh,
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When `on_token_expired` is provided, the SDK calls it on 401 responses and retries the request with the new token. Concurrent 401s are deduplicated — only one refresh runs at a time.
|
|
103
|
+
|
|
104
|
+
## Scoped Client Pattern
|
|
105
|
+
|
|
106
|
+
The SDK mirrors the API's resource hierarchy. Navigate with chained accessors:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Account-level resources
|
|
110
|
+
await admin.issuers.list()
|
|
111
|
+
await admin.webhooks.create({"url": "https://..."})
|
|
112
|
+
await admin.events.list(limit=50)
|
|
113
|
+
|
|
114
|
+
# Issuer scope
|
|
115
|
+
iss = admin.issuer("iss_xxx")
|
|
116
|
+
await iss.users.list()
|
|
117
|
+
await iss.agents.create({"name": "bot"})
|
|
118
|
+
await iss.clients.list()
|
|
119
|
+
await iss.organizations.list()
|
|
120
|
+
|
|
121
|
+
# User scope (nested under issuer)
|
|
122
|
+
usr = admin.issuer("iss_xxx").user("usr_xxx")
|
|
123
|
+
await usr.get()
|
|
124
|
+
await usr.sessions.list()
|
|
125
|
+
await usr.tokens.list()
|
|
126
|
+
await usr.trusted_devices.list()
|
|
127
|
+
await usr.verifiers.list()
|
|
128
|
+
|
|
129
|
+
# Webhook scope
|
|
130
|
+
wh = admin.webhook("wh_xxx")
|
|
131
|
+
await wh.get()
|
|
132
|
+
await wh.deliveries.list()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Pagination
|
|
136
|
+
|
|
137
|
+
List endpoints return a `Page` with cursor-based pagination:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# Manual pagination
|
|
141
|
+
page = await admin.issuer("iss_xxx").users.list(limit=25)
|
|
142
|
+
print(page.data) # list[dict]
|
|
143
|
+
print(page.has_more) # bool
|
|
144
|
+
print(page.next_cursor) # str | None
|
|
145
|
+
|
|
146
|
+
# Fetch next page
|
|
147
|
+
if page.has_more:
|
|
148
|
+
next_page = await admin.issuer("iss_xxx").users.list(
|
|
149
|
+
limit=25, cursor=page.next_cursor
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Auto-pagination (yields individual items across all pages)
|
|
153
|
+
async for user in admin.issuer("iss_xxx").users.list_all():
|
|
154
|
+
print(user)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Retries
|
|
158
|
+
|
|
159
|
+
Read-only requests (GET, HEAD, OPTIONS) are automatically retried on 429, 502, 503, and 504 responses with exponential backoff. The `Retry-After` header is respected when present.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Default: retries enabled (3 attempts, 1s base delay, exponential backoff)
|
|
163
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
164
|
+
|
|
165
|
+
# Disable retries
|
|
166
|
+
admin = AuthPIAdmin(api_key="key_xxx", retries=False)
|
|
167
|
+
|
|
168
|
+
# Custom retry config
|
|
169
|
+
admin = AuthPIAdmin(
|
|
170
|
+
api_key="key_xxx",
|
|
171
|
+
retries={"limit": 5, "delay": 0.5, "backoff": "linear"},
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Mutations (POST, PATCH, DELETE) are never retried automatically. Use idempotency keys and handle retries explicitly for writes.
|
|
176
|
+
|
|
177
|
+
## ETags & Optimistic Concurrency
|
|
178
|
+
|
|
179
|
+
GET responses include an `_etag` field. Pass it back on updates to prevent overwriting concurrent changes:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
user = await admin.issuer("iss_xxx").user("usr_xxx").get()
|
|
183
|
+
|
|
184
|
+
# Conditional update — fails with PreconditionFailedError if modified
|
|
185
|
+
await admin.issuer("iss_xxx").users.update(
|
|
186
|
+
"usr_xxx",
|
|
187
|
+
{"display_name": "Bob"},
|
|
188
|
+
if_match=user.get("_etag"),
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Error Handling
|
|
193
|
+
|
|
194
|
+
The SDK maps HTTP status codes to specific error classes:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from authpi_admin import (
|
|
198
|
+
NotFoundError,
|
|
199
|
+
ValidationError,
|
|
200
|
+
AuthenticationError,
|
|
201
|
+
RateLimitError,
|
|
202
|
+
PreconditionFailedError,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
await admin.issuer("iss_xxx").user("usr_xxx").get()
|
|
207
|
+
except NotFoundError:
|
|
208
|
+
print("User not found")
|
|
209
|
+
except ValidationError as err:
|
|
210
|
+
print("Validation failed:", err.fields)
|
|
211
|
+
except RateLimitError as err:
|
|
212
|
+
print(f"Retry after {err.retry_after} seconds")
|
|
213
|
+
except AuthenticationError:
|
|
214
|
+
print("Invalid credentials")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Error Hierarchy
|
|
218
|
+
|
|
219
|
+
| Error | Status | Extra Fields | Retryable |
|
|
220
|
+
|-------|--------|--------------|-----------|
|
|
221
|
+
| `ApiError` | — | `error`, `error_description`, `status_code`, `retryable`, `reference`, `raw_body` | — |
|
|
222
|
+
| `ValidationError` | 400, 422 | `fields` | No |
|
|
223
|
+
| `AuthenticationError` | 401 | — | No |
|
|
224
|
+
| `ForbiddenError` | 403 | — | No |
|
|
225
|
+
| `NotFoundError` | 404 | — | No |
|
|
226
|
+
| `ConflictError` | 409 | — | No |
|
|
227
|
+
| `PreconditionFailedError` | 412 | `current_etag` | No |
|
|
228
|
+
| `RateLimitError` | 429 | `retry_after` | Yes |
|
|
229
|
+
| `InternalServerError` | 500 | — | No |
|
|
230
|
+
| `BadGatewayError` | 502 | — | Yes |
|
|
231
|
+
| `ServiceUnavailableError` | 503 | — | Yes |
|
|
232
|
+
| `GatewayTimeoutError` | 504 | — | Yes |
|
|
233
|
+
| `UnexpectedError` | other | — | No |
|
|
234
|
+
| `ClosedClientError` | — | — | No |
|
|
235
|
+
|
|
236
|
+
## Configuration
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from authpi_admin import AuthPIAdmin
|
|
240
|
+
|
|
241
|
+
admin = AuthPIAdmin(
|
|
242
|
+
api_key="key_xxx", # or access_token + account_id
|
|
243
|
+
base_url="https://api.authpi.dev", # default
|
|
244
|
+
timeout=30.0, # default, in seconds
|
|
245
|
+
default_headers={"X-Custom": "value"}, # optional extra headers
|
|
246
|
+
retries=True, # default (or False, or dict)
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Custom httpx Client
|
|
251
|
+
|
|
252
|
+
Inject a pre-configured `httpx.AsyncClient` for advanced use cases (proxies, certificates, connection pooling):
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
import httpx
|
|
256
|
+
from authpi_admin import AuthPIAdmin
|
|
257
|
+
|
|
258
|
+
async with httpx.AsyncClient(proxies="http://proxy:8080") as http:
|
|
259
|
+
admin = AuthPIAdmin(api_key="key_xxx", http_client=http)
|
|
260
|
+
await admin.issuers.list()
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Context Manager
|
|
264
|
+
|
|
265
|
+
The SDK supports async context managers for clean resource cleanup:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
async with AuthPIAdmin(api_key="key_xxx") as admin:
|
|
269
|
+
await admin.issuers.list()
|
|
270
|
+
# httpx client is closed automatically
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Or close manually:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
277
|
+
try:
|
|
278
|
+
await admin.issuers.list()
|
|
279
|
+
finally:
|
|
280
|
+
await admin.close()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# authpi-admin
|
|
2
|
+
|
|
3
|
+
Official Python Admin SDK for the AuthPI Core API.
|
|
4
|
+
|
|
5
|
+
**Requirements:** Python 3.11+, async-only (`httpx` + `asyncio`)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install authpi-admin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from authpi_admin import AuthPIAdmin
|
|
17
|
+
|
|
18
|
+
async with AuthPIAdmin(api_key="key_xxx") as admin:
|
|
19
|
+
# List issuers
|
|
20
|
+
page = await admin.issuers.list(limit=10)
|
|
21
|
+
print(page.data)
|
|
22
|
+
|
|
23
|
+
# Scope into an issuer and manage users
|
|
24
|
+
users = await admin.issuer("iss_xxx").users.list()
|
|
25
|
+
|
|
26
|
+
# Auto-paginate
|
|
27
|
+
async for user in admin.issuer("iss_xxx").users.list_all():
|
|
28
|
+
print(user)
|
|
29
|
+
|
|
30
|
+
# Create a user
|
|
31
|
+
user = await admin.issuer("iss_xxx").users.create({
|
|
32
|
+
"email": "alice@example.com",
|
|
33
|
+
"display_name": "Alice",
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
### API Key (default)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Bearer Token
|
|
46
|
+
|
|
47
|
+
For server-side applications authenticating on behalf of a user session:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
admin = AuthPIAdmin(
|
|
51
|
+
access_token="tok_xxx",
|
|
52
|
+
account_id="acc_xxx",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With optional token refresh callback:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
async def refresh():
|
|
60
|
+
new_tokens = await my_refresh_logic()
|
|
61
|
+
return {"access_token": new_tokens.access_token}
|
|
62
|
+
|
|
63
|
+
admin = AuthPIAdmin(
|
|
64
|
+
access_token="tok_xxx",
|
|
65
|
+
account_id="acc_xxx",
|
|
66
|
+
on_token_expired=refresh,
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When `on_token_expired` is provided, the SDK calls it on 401 responses and retries the request with the new token. Concurrent 401s are deduplicated — only one refresh runs at a time.
|
|
71
|
+
|
|
72
|
+
## Scoped Client Pattern
|
|
73
|
+
|
|
74
|
+
The SDK mirrors the API's resource hierarchy. Navigate with chained accessors:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Account-level resources
|
|
78
|
+
await admin.issuers.list()
|
|
79
|
+
await admin.webhooks.create({"url": "https://..."})
|
|
80
|
+
await admin.events.list(limit=50)
|
|
81
|
+
|
|
82
|
+
# Issuer scope
|
|
83
|
+
iss = admin.issuer("iss_xxx")
|
|
84
|
+
await iss.users.list()
|
|
85
|
+
await iss.agents.create({"name": "bot"})
|
|
86
|
+
await iss.clients.list()
|
|
87
|
+
await iss.organizations.list()
|
|
88
|
+
|
|
89
|
+
# User scope (nested under issuer)
|
|
90
|
+
usr = admin.issuer("iss_xxx").user("usr_xxx")
|
|
91
|
+
await usr.get()
|
|
92
|
+
await usr.sessions.list()
|
|
93
|
+
await usr.tokens.list()
|
|
94
|
+
await usr.trusted_devices.list()
|
|
95
|
+
await usr.verifiers.list()
|
|
96
|
+
|
|
97
|
+
# Webhook scope
|
|
98
|
+
wh = admin.webhook("wh_xxx")
|
|
99
|
+
await wh.get()
|
|
100
|
+
await wh.deliveries.list()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Pagination
|
|
104
|
+
|
|
105
|
+
List endpoints return a `Page` with cursor-based pagination:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Manual pagination
|
|
109
|
+
page = await admin.issuer("iss_xxx").users.list(limit=25)
|
|
110
|
+
print(page.data) # list[dict]
|
|
111
|
+
print(page.has_more) # bool
|
|
112
|
+
print(page.next_cursor) # str | None
|
|
113
|
+
|
|
114
|
+
# Fetch next page
|
|
115
|
+
if page.has_more:
|
|
116
|
+
next_page = await admin.issuer("iss_xxx").users.list(
|
|
117
|
+
limit=25, cursor=page.next_cursor
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Auto-pagination (yields individual items across all pages)
|
|
121
|
+
async for user in admin.issuer("iss_xxx").users.list_all():
|
|
122
|
+
print(user)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Retries
|
|
126
|
+
|
|
127
|
+
Read-only requests (GET, HEAD, OPTIONS) are automatically retried on 429, 502, 503, and 504 responses with exponential backoff. The `Retry-After` header is respected when present.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Default: retries enabled (3 attempts, 1s base delay, exponential backoff)
|
|
131
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
132
|
+
|
|
133
|
+
# Disable retries
|
|
134
|
+
admin = AuthPIAdmin(api_key="key_xxx", retries=False)
|
|
135
|
+
|
|
136
|
+
# Custom retry config
|
|
137
|
+
admin = AuthPIAdmin(
|
|
138
|
+
api_key="key_xxx",
|
|
139
|
+
retries={"limit": 5, "delay": 0.5, "backoff": "linear"},
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Mutations (POST, PATCH, DELETE) are never retried automatically. Use idempotency keys and handle retries explicitly for writes.
|
|
144
|
+
|
|
145
|
+
## ETags & Optimistic Concurrency
|
|
146
|
+
|
|
147
|
+
GET responses include an `_etag` field. Pass it back on updates to prevent overwriting concurrent changes:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
user = await admin.issuer("iss_xxx").user("usr_xxx").get()
|
|
151
|
+
|
|
152
|
+
# Conditional update — fails with PreconditionFailedError if modified
|
|
153
|
+
await admin.issuer("iss_xxx").users.update(
|
|
154
|
+
"usr_xxx",
|
|
155
|
+
{"display_name": "Bob"},
|
|
156
|
+
if_match=user.get("_etag"),
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Error Handling
|
|
161
|
+
|
|
162
|
+
The SDK maps HTTP status codes to specific error classes:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from authpi_admin import (
|
|
166
|
+
NotFoundError,
|
|
167
|
+
ValidationError,
|
|
168
|
+
AuthenticationError,
|
|
169
|
+
RateLimitError,
|
|
170
|
+
PreconditionFailedError,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
await admin.issuer("iss_xxx").user("usr_xxx").get()
|
|
175
|
+
except NotFoundError:
|
|
176
|
+
print("User not found")
|
|
177
|
+
except ValidationError as err:
|
|
178
|
+
print("Validation failed:", err.fields)
|
|
179
|
+
except RateLimitError as err:
|
|
180
|
+
print(f"Retry after {err.retry_after} seconds")
|
|
181
|
+
except AuthenticationError:
|
|
182
|
+
print("Invalid credentials")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Error Hierarchy
|
|
186
|
+
|
|
187
|
+
| Error | Status | Extra Fields | Retryable |
|
|
188
|
+
|-------|--------|--------------|-----------|
|
|
189
|
+
| `ApiError` | — | `error`, `error_description`, `status_code`, `retryable`, `reference`, `raw_body` | — |
|
|
190
|
+
| `ValidationError` | 400, 422 | `fields` | No |
|
|
191
|
+
| `AuthenticationError` | 401 | — | No |
|
|
192
|
+
| `ForbiddenError` | 403 | — | No |
|
|
193
|
+
| `NotFoundError` | 404 | — | No |
|
|
194
|
+
| `ConflictError` | 409 | — | No |
|
|
195
|
+
| `PreconditionFailedError` | 412 | `current_etag` | No |
|
|
196
|
+
| `RateLimitError` | 429 | `retry_after` | Yes |
|
|
197
|
+
| `InternalServerError` | 500 | — | No |
|
|
198
|
+
| `BadGatewayError` | 502 | — | Yes |
|
|
199
|
+
| `ServiceUnavailableError` | 503 | — | Yes |
|
|
200
|
+
| `GatewayTimeoutError` | 504 | — | Yes |
|
|
201
|
+
| `UnexpectedError` | other | — | No |
|
|
202
|
+
| `ClosedClientError` | — | — | No |
|
|
203
|
+
|
|
204
|
+
## Configuration
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from authpi_admin import AuthPIAdmin
|
|
208
|
+
|
|
209
|
+
admin = AuthPIAdmin(
|
|
210
|
+
api_key="key_xxx", # or access_token + account_id
|
|
211
|
+
base_url="https://api.authpi.dev", # default
|
|
212
|
+
timeout=30.0, # default, in seconds
|
|
213
|
+
default_headers={"X-Custom": "value"}, # optional extra headers
|
|
214
|
+
retries=True, # default (or False, or dict)
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Custom httpx Client
|
|
219
|
+
|
|
220
|
+
Inject a pre-configured `httpx.AsyncClient` for advanced use cases (proxies, certificates, connection pooling):
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import httpx
|
|
224
|
+
from authpi_admin import AuthPIAdmin
|
|
225
|
+
|
|
226
|
+
async with httpx.AsyncClient(proxies="http://proxy:8080") as http:
|
|
227
|
+
admin = AuthPIAdmin(api_key="key_xxx", http_client=http)
|
|
228
|
+
await admin.issuers.list()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Context Manager
|
|
232
|
+
|
|
233
|
+
The SDK supports async context managers for clean resource cleanup:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
async with AuthPIAdmin(api_key="key_xxx") as admin:
|
|
237
|
+
await admin.issuers.list()
|
|
238
|
+
# httpx client is closed automatically
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Or close manually:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
admin = AuthPIAdmin(api_key="key_xxx")
|
|
245
|
+
try:
|
|
246
|
+
await admin.issuers.list()
|
|
247
|
+
finally:
|
|
248
|
+
await admin.close()
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""AuthPI Admin SDK — Official Python client for the AuthPI Core API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.3.0"
|
|
6
|
+
|
|
7
|
+
from authpi_admin.client import AuthPIAdmin
|
|
8
|
+
from authpi_admin.errors import (
|
|
9
|
+
ApiError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
BadGatewayError,
|
|
12
|
+
ClosedClientError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
ConflictError,
|
|
15
|
+
ForbiddenError,
|
|
16
|
+
GatewayTimeoutError,
|
|
17
|
+
InternalServerError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
PreconditionFailedError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
ServiceUnavailableError,
|
|
23
|
+
UnexpectedError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
26
|
+
from authpi_admin.generated.scopes.issuer_scope import IssuerScope
|
|
27
|
+
from authpi_admin.generated.scopes.user_scope import UserScope
|
|
28
|
+
from authpi_admin.generated.scopes.agent_scope import AgentScope
|
|
29
|
+
from authpi_admin.generated.scopes.webhook_scope import WebhookScope
|
|
30
|
+
from authpi_admin.pagination import Page
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AgentScope",
|
|
34
|
+
"ApiError",
|
|
35
|
+
"AuthPIAdmin",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"BadGatewayError",
|
|
38
|
+
"ClosedClientError",
|
|
39
|
+
"ConfigurationError",
|
|
40
|
+
"ConflictError",
|
|
41
|
+
"ForbiddenError",
|
|
42
|
+
"GatewayTimeoutError",
|
|
43
|
+
"InternalServerError",
|
|
44
|
+
"IssuerScope",
|
|
45
|
+
"NotFoundError",
|
|
46
|
+
"Page",
|
|
47
|
+
"PreconditionFailedError",
|
|
48
|
+
"RateLimitError",
|
|
49
|
+
"ServerError",
|
|
50
|
+
"ServiceUnavailableError",
|
|
51
|
+
"UnexpectedError",
|
|
52
|
+
"UserScope",
|
|
53
|
+
"ValidationError",
|
|
54
|
+
"WebhookScope",
|
|
55
|
+
]
|