aho-sdk 0.1.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.
Files changed (38) hide show
  1. aho_sdk-0.1.0/PKG-INFO +380 -0
  2. aho_sdk-0.1.0/README.md +341 -0
  3. aho_sdk-0.1.0/aho_sdk/__init__.py +108 -0
  4. aho_sdk-0.1.0/aho_sdk/account.py +180 -0
  5. aho_sdk-0.1.0/aho_sdk/async_account.py +194 -0
  6. aho_sdk-0.1.0/aho_sdk/async_cursor_page.py +99 -0
  7. aho_sdk-0.1.0/aho_sdk/async_holder.py +112 -0
  8. aho_sdk-0.1.0/aho_sdk/async_http_client.py +262 -0
  9. aho_sdk-0.1.0/aho_sdk/async_issuer.py +388 -0
  10. aho_sdk-0.1.0/aho_sdk/async_page.py +100 -0
  11. aho_sdk-0.1.0/aho_sdk/async_public.py +81 -0
  12. aho_sdk-0.1.0/aho_sdk/async_schemas.py +96 -0
  13. aho_sdk-0.1.0/aho_sdk/async_system.py +73 -0
  14. aho_sdk-0.1.0/aho_sdk/async_verifier.py +147 -0
  15. aho_sdk-0.1.0/aho_sdk/cursor_page.py +98 -0
  16. aho_sdk-0.1.0/aho_sdk/exceptions.py +140 -0
  17. aho_sdk-0.1.0/aho_sdk/holder.py +98 -0
  18. aho_sdk-0.1.0/aho_sdk/http_client.py +437 -0
  19. aho_sdk-0.1.0/aho_sdk/issuer.py +374 -0
  20. aho_sdk-0.1.0/aho_sdk/models.py +763 -0
  21. aho_sdk-0.1.0/aho_sdk/page.py +99 -0
  22. aho_sdk-0.1.0/aho_sdk/public.py +67 -0
  23. aho_sdk-0.1.0/aho_sdk/schemas.py +82 -0
  24. aho_sdk-0.1.0/aho_sdk/system.py +59 -0
  25. aho_sdk-0.1.0/aho_sdk/types.py +981 -0
  26. aho_sdk-0.1.0/aho_sdk/verifier.py +133 -0
  27. aho_sdk-0.1.0/aho_sdk/version.py +4 -0
  28. aho_sdk-0.1.0/aho_sdk.egg-info/PKG-INFO +380 -0
  29. aho_sdk-0.1.0/aho_sdk.egg-info/SOURCES.txt +36 -0
  30. aho_sdk-0.1.0/aho_sdk.egg-info/dependency_links.txt +1 -0
  31. aho_sdk-0.1.0/aho_sdk.egg-info/requires.txt +20 -0
  32. aho_sdk-0.1.0/aho_sdk.egg-info/top_level.txt +1 -0
  33. aho_sdk-0.1.0/pyproject.toml +75 -0
  34. aho_sdk-0.1.0/setup.cfg +4 -0
  35. aho_sdk-0.1.0/tests/test_clients.py +217 -0
  36. aho_sdk-0.1.0/tests/test_exceptions.py +234 -0
  37. aho_sdk-0.1.0/tests/test_http_client.py +276 -0
  38. aho_sdk-0.1.0/tests/test_page.py +164 -0
aho_sdk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,380 @@
1
+ Metadata-Version: 2.4
2
+ Name: aho-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Aho Verifiable Credentials API
5
+ Author-email: Aho <support@aho.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://aho.com
8
+ Project-URL: Documentation, https://aho.com/developers/python-sdk
9
+ Project-URL: Repository, https://github.com/aho-hq/aho-python
10
+ Project-URL: Changelog, https://github.com/aho-hq/aho-python/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: async
24
+ Requires-Dist: httpx>=0.25.0; extra == "async"
25
+ Provides-Extra: validation
26
+ Requires-Dist: pydantic>=2.0; extra == "validation"
27
+ Provides-Extra: all
28
+ Requires-Dist: httpx>=0.25.0; extra == "all"
29
+ Requires-Dist: pydantic>=2.0; extra == "all"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
34
+ Requires-Dist: responses>=0.23; extra == "dev"
35
+ Requires-Dist: httpx>=0.25.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.1; extra == "dev"
38
+ Requires-Dist: pydantic>=2.0; extra == "dev"
39
+
40
+ # aho_sdk Python SDK
41
+
42
+ Official Python SDK for the [Aho](https://aho.com) Verifiable Credentials API.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ # Basic installation (sync only)
48
+ pip install aho-sdk
49
+
50
+ # With async support
51
+ pip install aho-sdk[async]
52
+
53
+ # With validation (Pydantic models)
54
+ pip install aho-sdk[validation]
55
+
56
+ # All features
57
+ pip install aho-sdk[all]
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ import os
64
+ from aho_sdk import Issuer
65
+
66
+ # Initialize the Issuer client
67
+ issuer = Issuer(api_key=os.environ['AHO_ISSUER_API_KEY'])
68
+
69
+ # Issue a credential
70
+ credential = issuer.credentials.create(
71
+ schema_uuid='your-schema-uuid',
72
+ subject_identifier='user@example.com',
73
+ claims={
74
+ 'name': 'Jane Doe',
75
+ 'role': 'Engineer'
76
+ }
77
+ )
78
+
79
+ print(credential['uuid'])
80
+ ```
81
+
82
+ ## Clients
83
+
84
+ The SDK provides the following clients:
85
+
86
+ | Client | Purpose | API Key Type |
87
+ |--------|---------|--------------|
88
+ | `Account` | Manage account settings, domains, and API keys | Account API Key |
89
+ | `System` | System health and status endpoints | System API Key |
90
+ | `Holder` | Manage holder credentials and presentations | Holder API Key |
91
+ | `Verifier` | Create presentation requests and verify credentials | Verifier API Key |
92
+ | `Issuer` | Issue and manage verifiable credentials | Issuer API Key |
93
+ | `Schemas` | Browse and retrieve credential schemas | Schemas API Key |
94
+ | `Public` | Public endpoints (no authentication required) | None (public) |
95
+
96
+ ## Usage Examples
97
+
98
+ ### Issuing Credentials
99
+
100
+ ```python
101
+ from aho_sdk import Issuer
102
+
103
+ issuer = Issuer(api_key=os.environ['AHO_ISSUER_API_KEY'])
104
+
105
+ # List all schemas
106
+ schemas = issuer.schemas.list()
107
+ for schema in schemas:
108
+ print(schema['name'])
109
+
110
+ # Create a schema
111
+ schema = issuer.schemas.create(
112
+ name='EmployeeBadge',
113
+ claims=[
114
+ {'name': 'employee_id', 'type': 'string', 'required': True},
115
+ {'name': 'department', 'type': 'string', 'required': True},
116
+ {'name': 'hire_date', 'type': 'date', 'required': False}
117
+ ]
118
+ )
119
+
120
+ # Issue a credential
121
+ credential = issuer.credentials.create(
122
+ schema_uuid=schema['uuid'],
123
+ subject_identifier='jane.doe@company.com',
124
+ claims={
125
+ 'employee_id': 'EMP-12345',
126
+ 'department': 'Engineering',
127
+ 'hire_date': '2024-01-15'
128
+ }
129
+ )
130
+
131
+ # Revoke a credential
132
+ issuer.credentials.revoke(uuid=credential['uuid'], reason='Employee departed')
133
+ ```
134
+
135
+ ### Verifying Credentials
136
+
137
+ ```python
138
+ from aho_sdk import Verifier
139
+
140
+ verifier = Verifier(api_key=os.environ['AHO_VERIFIER_API_KEY'])
141
+
142
+ # Create a presentation request
143
+ request = verifier.requests.create(
144
+ name='Employment Verification',
145
+ query_format='dcql',
146
+ credentials=[
147
+ {
148
+ 'id': 'employee_badge',
149
+ 'format': 'vc+sd-jwt',
150
+ 'claims': [
151
+ {'path': ['employee_id']},
152
+ {'path': ['department']}
153
+ ]
154
+ }
155
+ ]
156
+ )
157
+
158
+ # Get the QR code for the request (supports 'png', 'svg' formats)
159
+ qr = verifier.requests.qr_code(uuid=request['uuid'], output_format='svg')
160
+
161
+ # List responses to the request
162
+ responses = verifier.responses.list(request_uuid=request['uuid'])
163
+ ```
164
+
165
+ ### Managing Holder Credentials
166
+
167
+ ```python
168
+ from aho_sdk import Holder
169
+
170
+ holder = Holder(api_key=os.environ['AHO_HOLDER_API_KEY'])
171
+
172
+ # List credentials
173
+ credentials = holder.credentials.list(status='active')
174
+
175
+ # Create a presentation (selective disclosure)
176
+ presentation = holder.presentations.create(
177
+ credential_uuid='credential-uuid',
178
+ disclosed_claims=['name', 'department']
179
+ )
180
+ ```
181
+
182
+ ### Account Management
183
+
184
+ ```python
185
+ from aho_sdk import Account
186
+
187
+ account = Account(api_key=os.environ['AHO_API_KEY'])
188
+
189
+ # Manage domains
190
+ domains = account.domains.list()
191
+ account.domains.verify(id=domain['id'])
192
+
193
+ # Manage signing keys
194
+ keys = account.signing_keys.list()
195
+ account.signing_keys.rotate(id=key['id'])
196
+
197
+ # Configure webhooks
198
+ account.webhooks.create(
199
+ url='https://your-app.com/webhooks/aho',
200
+ events=['credential.issued', 'credential.revoked']
201
+ )
202
+ ```
203
+
204
+ ## Pagination
205
+
206
+ List methods return `Page` objects with built-in iteration:
207
+
208
+ ```python
209
+ # Iterate through all pages automatically
210
+ for credential in issuer.credentials.list():
211
+ print(credential['uuid'])
212
+
213
+ # Or handle pages manually
214
+ page = issuer.credentials.list(per_page=50)
215
+ while page:
216
+ for credential in page.data:
217
+ print(credential['uuid'])
218
+ page = page.next_page()
219
+
220
+ # Collect all items
221
+ all_credentials = list(issuer.credentials.list())
222
+ ```
223
+
224
+ ## File Uploads
225
+
226
+ For endpoints that accept file uploads:
227
+
228
+ ```python
229
+ # Upload a file
230
+ issuer.media.upload(
231
+ file=open('document.pdf', 'rb'),
232
+ metadata={'description': 'Employee contract'}
233
+ )
234
+
235
+ # The SDK auto-detects MIME types from file extensions
236
+ # You can also pass a Path:
237
+ from pathlib import Path
238
+ issuer.media.upload(file=Path('document.pdf'))
239
+ ```
240
+
241
+ ## Binary Responses
242
+
243
+ Some endpoints return binary data (images, PDFs):
244
+
245
+ ```python
246
+ # Get QR code as PNG
247
+ png_data = verifier.requests.qr_code(uuid='...', output_format='png')
248
+ with open('qr.png', 'wb') as f:
249
+ f.write(png_data)
250
+
251
+ # Get QR code as SVG
252
+ svg_data = verifier.requests.qr_code(uuid='...', output_format='svg')
253
+ with open('qr.svg', 'w') as f:
254
+ f.write(svg_data.decode('utf-8'))
255
+ ```
256
+
257
+ ## Error Handling
258
+
259
+ ```python
260
+ from aho_sdk import (
261
+ Issuer,
262
+ ValidationError,
263
+ AuthenticationError,
264
+ NotFoundError,
265
+ RateLimitError,
266
+ ApiError
267
+ )
268
+
269
+ try:
270
+ issuer.credentials.create(invalid_params)
271
+ except ValidationError as e:
272
+ # 422 - Validation failed
273
+ for error in e.field_errors:
274
+ print(f"{error['field']}: {error['hint']}")
275
+ except AuthenticationError as e:
276
+ # 401 - Invalid API key
277
+ print("Check your API key")
278
+ except NotFoundError as e:
279
+ # 404 - Resource not found
280
+ print(f"Resource not found: {e.message}")
281
+ except RateLimitError as e:
282
+ # 429 - Rate limited (SDK auto-retries with exponential backoff)
283
+ print(f"Retry after {e.retry_after} seconds")
284
+ except ApiError as e:
285
+ # Other API errors
286
+ print(f"Error {e.status_code}: {e.message}")
287
+ print(f"Request ID: {e.request_id}")
288
+ ```
289
+
290
+ ### Error Classes
291
+
292
+ | Error Class | HTTP Status | Description |
293
+ |-------------|-------------|-------------|
294
+ | `AuthenticationError` | 401 | Invalid or missing API key |
295
+ | `ForbiddenError` | 403 | Insufficient permissions |
296
+ | `NotFoundError` | 404 | Resource not found |
297
+ | `ConflictError` | 409 | Resource conflict |
298
+ | `ValidationError` | 422 | Request validation failed |
299
+ | `RateLimitError` | 429 | Rate limit exceeded |
300
+ | `ServerError` | 5xx | Server-side error |
301
+ | `NetworkError` | - | Connection/timeout error |
302
+ | `ApiError` | * | Base class for all API errors |
303
+
304
+ ## Rate Limiting
305
+
306
+ The SDK automatically handles rate limits with exponential backoff:
307
+
308
+ - **Idempotent methods** (GET, DELETE, PUT): Auto-retry up to 3 times
309
+ - **Non-idempotent methods** (POST, PATCH): Only retry with idempotency key
310
+
311
+ ```python
312
+ # Use idempotency keys for safe retries on POST/PATCH
313
+ issuer.credentials.create(
314
+ schema_uuid='...',
315
+ claims={...},
316
+ idempotency_key='unique-request-id'
317
+ )
318
+ ```
319
+
320
+ ## Configuration
321
+
322
+ ```python
323
+ import logging
324
+
325
+ # Custom configuration
326
+ issuer = Issuer(
327
+ api_key=os.environ['AHO_ISSUER_API_KEY'],
328
+ base_url='https://api.aho.com', # Custom base URL
329
+ timeout=60, # Request timeout in seconds
330
+ logger=logging.getLogger('aho') # Enable debug logging
331
+ )
332
+ ```
333
+
334
+ ## Async Support
335
+
336
+ The SDK provides async versions of all clients. Install with async support:
337
+
338
+ ```bash
339
+ pip install aho-sdk[async]
340
+ ```
341
+
342
+ Then use the async clients:
343
+
344
+ ```python
345
+ import asyncio
346
+ from aho_sdk import AsyncIssuer
347
+
348
+ async def main():
349
+ # Use async context manager for automatic cleanup
350
+ async with AsyncIssuer(api_key=os.environ['AHO_ISSUER_API_KEY']) as issuer:
351
+ # All methods are async
352
+ credentials = await issuer.credentials.list()
353
+
354
+ # Async iteration through pages
355
+ async for credential in credentials:
356
+ print(credential['uuid'])
357
+
358
+ # Or await individual operations
359
+ credential = await issuer.credentials.get(uuid='...')
360
+
361
+ asyncio.run(main())
362
+ ```
363
+
364
+ Async clients available:
365
+ - `AsyncAccount` - Async version of `Account`
366
+ - `AsyncSystem` - Async version of `System`
367
+ - `AsyncHolder` - Async version of `Holder`
368
+ - `AsyncVerifier` - Async version of `Verifier`
369
+ - `AsyncIssuer` - Async version of `Issuer`
370
+ - `AsyncSchemas` - Async version of `Schemas`
371
+ - `AsyncPublic` - Async version of `Public`
372
+
373
+ ## Requirements
374
+
375
+ - Python 3.9+
376
+ - httpx (for async support, optional)
377
+
378
+ ## License
379
+
380
+ MIT
@@ -0,0 +1,341 @@
1
+ # aho_sdk Python SDK
2
+
3
+ Official Python SDK for the [Aho](https://aho.com) Verifiable Credentials API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Basic installation (sync only)
9
+ pip install aho-sdk
10
+
11
+ # With async support
12
+ pip install aho-sdk[async]
13
+
14
+ # With validation (Pydantic models)
15
+ pip install aho-sdk[validation]
16
+
17
+ # All features
18
+ pip install aho-sdk[all]
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```python
24
+ import os
25
+ from aho_sdk import Issuer
26
+
27
+ # Initialize the Issuer client
28
+ issuer = Issuer(api_key=os.environ['AHO_ISSUER_API_KEY'])
29
+
30
+ # Issue a credential
31
+ credential = issuer.credentials.create(
32
+ schema_uuid='your-schema-uuid',
33
+ subject_identifier='user@example.com',
34
+ claims={
35
+ 'name': 'Jane Doe',
36
+ 'role': 'Engineer'
37
+ }
38
+ )
39
+
40
+ print(credential['uuid'])
41
+ ```
42
+
43
+ ## Clients
44
+
45
+ The SDK provides the following clients:
46
+
47
+ | Client | Purpose | API Key Type |
48
+ |--------|---------|--------------|
49
+ | `Account` | Manage account settings, domains, and API keys | Account API Key |
50
+ | `System` | System health and status endpoints | System API Key |
51
+ | `Holder` | Manage holder credentials and presentations | Holder API Key |
52
+ | `Verifier` | Create presentation requests and verify credentials | Verifier API Key |
53
+ | `Issuer` | Issue and manage verifiable credentials | Issuer API Key |
54
+ | `Schemas` | Browse and retrieve credential schemas | Schemas API Key |
55
+ | `Public` | Public endpoints (no authentication required) | None (public) |
56
+
57
+ ## Usage Examples
58
+
59
+ ### Issuing Credentials
60
+
61
+ ```python
62
+ from aho_sdk import Issuer
63
+
64
+ issuer = Issuer(api_key=os.environ['AHO_ISSUER_API_KEY'])
65
+
66
+ # List all schemas
67
+ schemas = issuer.schemas.list()
68
+ for schema in schemas:
69
+ print(schema['name'])
70
+
71
+ # Create a schema
72
+ schema = issuer.schemas.create(
73
+ name='EmployeeBadge',
74
+ claims=[
75
+ {'name': 'employee_id', 'type': 'string', 'required': True},
76
+ {'name': 'department', 'type': 'string', 'required': True},
77
+ {'name': 'hire_date', 'type': 'date', 'required': False}
78
+ ]
79
+ )
80
+
81
+ # Issue a credential
82
+ credential = issuer.credentials.create(
83
+ schema_uuid=schema['uuid'],
84
+ subject_identifier='jane.doe@company.com',
85
+ claims={
86
+ 'employee_id': 'EMP-12345',
87
+ 'department': 'Engineering',
88
+ 'hire_date': '2024-01-15'
89
+ }
90
+ )
91
+
92
+ # Revoke a credential
93
+ issuer.credentials.revoke(uuid=credential['uuid'], reason='Employee departed')
94
+ ```
95
+
96
+ ### Verifying Credentials
97
+
98
+ ```python
99
+ from aho_sdk import Verifier
100
+
101
+ verifier = Verifier(api_key=os.environ['AHO_VERIFIER_API_KEY'])
102
+
103
+ # Create a presentation request
104
+ request = verifier.requests.create(
105
+ name='Employment Verification',
106
+ query_format='dcql',
107
+ credentials=[
108
+ {
109
+ 'id': 'employee_badge',
110
+ 'format': 'vc+sd-jwt',
111
+ 'claims': [
112
+ {'path': ['employee_id']},
113
+ {'path': ['department']}
114
+ ]
115
+ }
116
+ ]
117
+ )
118
+
119
+ # Get the QR code for the request (supports 'png', 'svg' formats)
120
+ qr = verifier.requests.qr_code(uuid=request['uuid'], output_format='svg')
121
+
122
+ # List responses to the request
123
+ responses = verifier.responses.list(request_uuid=request['uuid'])
124
+ ```
125
+
126
+ ### Managing Holder Credentials
127
+
128
+ ```python
129
+ from aho_sdk import Holder
130
+
131
+ holder = Holder(api_key=os.environ['AHO_HOLDER_API_KEY'])
132
+
133
+ # List credentials
134
+ credentials = holder.credentials.list(status='active')
135
+
136
+ # Create a presentation (selective disclosure)
137
+ presentation = holder.presentations.create(
138
+ credential_uuid='credential-uuid',
139
+ disclosed_claims=['name', 'department']
140
+ )
141
+ ```
142
+
143
+ ### Account Management
144
+
145
+ ```python
146
+ from aho_sdk import Account
147
+
148
+ account = Account(api_key=os.environ['AHO_API_KEY'])
149
+
150
+ # Manage domains
151
+ domains = account.domains.list()
152
+ account.domains.verify(id=domain['id'])
153
+
154
+ # Manage signing keys
155
+ keys = account.signing_keys.list()
156
+ account.signing_keys.rotate(id=key['id'])
157
+
158
+ # Configure webhooks
159
+ account.webhooks.create(
160
+ url='https://your-app.com/webhooks/aho',
161
+ events=['credential.issued', 'credential.revoked']
162
+ )
163
+ ```
164
+
165
+ ## Pagination
166
+
167
+ List methods return `Page` objects with built-in iteration:
168
+
169
+ ```python
170
+ # Iterate through all pages automatically
171
+ for credential in issuer.credentials.list():
172
+ print(credential['uuid'])
173
+
174
+ # Or handle pages manually
175
+ page = issuer.credentials.list(per_page=50)
176
+ while page:
177
+ for credential in page.data:
178
+ print(credential['uuid'])
179
+ page = page.next_page()
180
+
181
+ # Collect all items
182
+ all_credentials = list(issuer.credentials.list())
183
+ ```
184
+
185
+ ## File Uploads
186
+
187
+ For endpoints that accept file uploads:
188
+
189
+ ```python
190
+ # Upload a file
191
+ issuer.media.upload(
192
+ file=open('document.pdf', 'rb'),
193
+ metadata={'description': 'Employee contract'}
194
+ )
195
+
196
+ # The SDK auto-detects MIME types from file extensions
197
+ # You can also pass a Path:
198
+ from pathlib import Path
199
+ issuer.media.upload(file=Path('document.pdf'))
200
+ ```
201
+
202
+ ## Binary Responses
203
+
204
+ Some endpoints return binary data (images, PDFs):
205
+
206
+ ```python
207
+ # Get QR code as PNG
208
+ png_data = verifier.requests.qr_code(uuid='...', output_format='png')
209
+ with open('qr.png', 'wb') as f:
210
+ f.write(png_data)
211
+
212
+ # Get QR code as SVG
213
+ svg_data = verifier.requests.qr_code(uuid='...', output_format='svg')
214
+ with open('qr.svg', 'w') as f:
215
+ f.write(svg_data.decode('utf-8'))
216
+ ```
217
+
218
+ ## Error Handling
219
+
220
+ ```python
221
+ from aho_sdk import (
222
+ Issuer,
223
+ ValidationError,
224
+ AuthenticationError,
225
+ NotFoundError,
226
+ RateLimitError,
227
+ ApiError
228
+ )
229
+
230
+ try:
231
+ issuer.credentials.create(invalid_params)
232
+ except ValidationError as e:
233
+ # 422 - Validation failed
234
+ for error in e.field_errors:
235
+ print(f"{error['field']}: {error['hint']}")
236
+ except AuthenticationError as e:
237
+ # 401 - Invalid API key
238
+ print("Check your API key")
239
+ except NotFoundError as e:
240
+ # 404 - Resource not found
241
+ print(f"Resource not found: {e.message}")
242
+ except RateLimitError as e:
243
+ # 429 - Rate limited (SDK auto-retries with exponential backoff)
244
+ print(f"Retry after {e.retry_after} seconds")
245
+ except ApiError as e:
246
+ # Other API errors
247
+ print(f"Error {e.status_code}: {e.message}")
248
+ print(f"Request ID: {e.request_id}")
249
+ ```
250
+
251
+ ### Error Classes
252
+
253
+ | Error Class | HTTP Status | Description |
254
+ |-------------|-------------|-------------|
255
+ | `AuthenticationError` | 401 | Invalid or missing API key |
256
+ | `ForbiddenError` | 403 | Insufficient permissions |
257
+ | `NotFoundError` | 404 | Resource not found |
258
+ | `ConflictError` | 409 | Resource conflict |
259
+ | `ValidationError` | 422 | Request validation failed |
260
+ | `RateLimitError` | 429 | Rate limit exceeded |
261
+ | `ServerError` | 5xx | Server-side error |
262
+ | `NetworkError` | - | Connection/timeout error |
263
+ | `ApiError` | * | Base class for all API errors |
264
+
265
+ ## Rate Limiting
266
+
267
+ The SDK automatically handles rate limits with exponential backoff:
268
+
269
+ - **Idempotent methods** (GET, DELETE, PUT): Auto-retry up to 3 times
270
+ - **Non-idempotent methods** (POST, PATCH): Only retry with idempotency key
271
+
272
+ ```python
273
+ # Use idempotency keys for safe retries on POST/PATCH
274
+ issuer.credentials.create(
275
+ schema_uuid='...',
276
+ claims={...},
277
+ idempotency_key='unique-request-id'
278
+ )
279
+ ```
280
+
281
+ ## Configuration
282
+
283
+ ```python
284
+ import logging
285
+
286
+ # Custom configuration
287
+ issuer = Issuer(
288
+ api_key=os.environ['AHO_ISSUER_API_KEY'],
289
+ base_url='https://api.aho.com', # Custom base URL
290
+ timeout=60, # Request timeout in seconds
291
+ logger=logging.getLogger('aho') # Enable debug logging
292
+ )
293
+ ```
294
+
295
+ ## Async Support
296
+
297
+ The SDK provides async versions of all clients. Install with async support:
298
+
299
+ ```bash
300
+ pip install aho-sdk[async]
301
+ ```
302
+
303
+ Then use the async clients:
304
+
305
+ ```python
306
+ import asyncio
307
+ from aho_sdk import AsyncIssuer
308
+
309
+ async def main():
310
+ # Use async context manager for automatic cleanup
311
+ async with AsyncIssuer(api_key=os.environ['AHO_ISSUER_API_KEY']) as issuer:
312
+ # All methods are async
313
+ credentials = await issuer.credentials.list()
314
+
315
+ # Async iteration through pages
316
+ async for credential in credentials:
317
+ print(credential['uuid'])
318
+
319
+ # Or await individual operations
320
+ credential = await issuer.credentials.get(uuid='...')
321
+
322
+ asyncio.run(main())
323
+ ```
324
+
325
+ Async clients available:
326
+ - `AsyncAccount` - Async version of `Account`
327
+ - `AsyncSystem` - Async version of `System`
328
+ - `AsyncHolder` - Async version of `Holder`
329
+ - `AsyncVerifier` - Async version of `Verifier`
330
+ - `AsyncIssuer` - Async version of `Issuer`
331
+ - `AsyncSchemas` - Async version of `Schemas`
332
+ - `AsyncPublic` - Async version of `Public`
333
+
334
+ ## Requirements
335
+
336
+ - Python 3.9+
337
+ - httpx (for async support, optional)
338
+
339
+ ## License
340
+
341
+ MIT