xAPI-client 1.0.11__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.
- xapi_client-1.0.11/LICENSE.md +12 -0
- xapi_client-1.0.11/PKG-INFO +625 -0
- xapi_client-1.0.11/README.md +598 -0
- xapi_client-1.0.11/pyproject.toml +75 -0
- xapi_client-1.0.11/setup.cfg +4 -0
- xapi_client-1.0.11/src/__init__.py +63 -0
- xapi_client-1.0.11/src/apiKey.py +90 -0
- xapi_client-1.0.11/src/client.py +286 -0
- xapi_client-1.0.11/src/endpoint.py +128 -0
- xapi_client-1.0.11/src/exceptions.py +221 -0
- xapi_client-1.0.11/src/logger.py +115 -0
- xapi_client-1.0.11/src/model.py +120 -0
- xapi_client-1.0.11/src/options.py +31 -0
- xapi_client-1.0.11/src/path.py +89 -0
- xapi_client-1.0.11/src/query.py +90 -0
- xapi_client-1.0.11/src/rateLimit.py +37 -0
- xapi_client-1.0.11/src/resource.py +178 -0
- xapi_client-1.0.11/src/retry.py +35 -0
- xapi_client-1.0.11/src/type.py +46 -0
- xapi_client-1.0.11/src/url.py +55 -0
- xapi_client-1.0.11/src/util.py +116 -0
- xapi_client-1.0.11/tests/test_api.py +572 -0
- xapi_client-1.0.11/xAPI_client.egg-info/PKG-INFO +625 -0
- xapi_client-1.0.11/xAPI_client.egg-info/SOURCES.txt +25 -0
- xapi_client-1.0.11/xAPI_client.egg-info/dependency_links.txt +1 -0
- xapi_client-1.0.11/xAPI_client.egg-info/requires.txt +4 -0
- xapi_client-1.0.11/xAPI_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright © 2026, Kohl Development Group.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
7
|
+
|
|
8
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
9
|
+
|
|
10
|
+
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
11
|
+
|
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xAPI-client
|
|
3
|
+
Version: 1.0.11
|
|
4
|
+
Summary: A lightweight, flexible asynchronous API client for Python built on httpx and pydantic
|
|
5
|
+
Author: rkohl
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/rkohl/xAPI
|
|
8
|
+
Project-URL: Source, https://github.com/rkohl/xAPI
|
|
9
|
+
Keywords: api,client,async,httpx,pydantic,rest,api-client,python-api-client
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Framework :: Pydantic :: 2
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE.md
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: loguru>=0.7
|
|
25
|
+
Requires-Dist: shortuuid>=1.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# xAPI
|
|
29
|
+
|
|
30
|
+
A lightweight, flexible asynchronous API client for Python built on [httpx](https://www.python-httpx.org/) and [pydantic](https://docs.pydantic.dev/).
|
|
31
|
+
|
|
32
|
+
xAPI organizes API endpoints into a tree of **Resources** and **Endpoints**, giving you a clean, dot-notation interface for calling any REST API with full type safety and automatic response validation.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
|
|
37
|
+
print(coin.name) # "Bitcoin"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Async-first** — Built on `httpx.AsyncClient` for high-performance, non-blocking requests.
|
|
45
|
+
- **Resource-oriented** — Organize endpoints into a hierarchical tree. Access them with dot notation (`client.coins.coin(...)`).
|
|
46
|
+
- **Type-safe responses** — Pydantic models validate and parse every API response automatically.
|
|
47
|
+
- **List responses** — Handle endpoints that return JSON arrays by passing a `list[Model]` response type.
|
|
48
|
+
- **Parameterized paths** — Define URL templates like `{id}` and inject values with type-safe `Parameters` enums.
|
|
49
|
+
- **Query parameters** — Build and manage query strings with the `Query` class.
|
|
50
|
+
- **Authentication** — Scoped API key auth — apply globally, per-endpoint, or disable entirely.
|
|
51
|
+
- **Rate limiting** — Built-in sliding window rate limiter to stay within API quotas.
|
|
52
|
+
- **Retry with backoff** — Automatic exponential backoff on 5xx errors, timeouts, and connection failures. 4xx errors are raised immediately.
|
|
53
|
+
- **Structured logging** — Color-coded, per-component logging via `loguru` (enabled with `debug=True`).
|
|
54
|
+
- **Context manager** — Proper connection cleanup with `async with` support.
|
|
55
|
+
|
|
56
|
+
**NOTE:** This is an experimental project and proof of concept. May not fully work as indended.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Install **xAPI** using pip
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
$ pip install xAPI
|
|
66
|
+
```
|
|
67
|
+
**Requirements:** Python 3.12+
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
import xAPI
|
|
76
|
+
|
|
77
|
+
from pydantic import BaseModel
|
|
78
|
+
|
|
79
|
+
# 1. Define a response model
|
|
80
|
+
class CoinModel(xAPI.ResponseModel):
|
|
81
|
+
id: str
|
|
82
|
+
symbol: str
|
|
83
|
+
name: str
|
|
84
|
+
|
|
85
|
+
# 2. Define path parameters
|
|
86
|
+
class CoinParams(xAPI.Parameters):
|
|
87
|
+
Bitcoin = "bitcoin"
|
|
88
|
+
Ethereum = "ethereum"
|
|
89
|
+
|
|
90
|
+
# 3. Define a path with parameter placeholders
|
|
91
|
+
class CoinPath(xAPI.Path[CoinParams]):
|
|
92
|
+
endpointPath: str = "{id}"
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
# 4. Create the client
|
|
96
|
+
auth = xAPI.APIKey(
|
|
97
|
+
key="x_cg_demo_api_key",
|
|
98
|
+
secret="YOUR_API_KEY",
|
|
99
|
+
scope="All",
|
|
100
|
+
schem="Header"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async with xAPI.Client(
|
|
104
|
+
url="https://api.example.com/v1/",
|
|
105
|
+
apiKey=auth
|
|
106
|
+
) as client:
|
|
107
|
+
|
|
108
|
+
# 5. Build resources and endpoints
|
|
109
|
+
coins = xAPI.Resource("coins")
|
|
110
|
+
coins.addEndpoints(
|
|
111
|
+
xAPI.Endpoint(name="coin", path=CoinPath(), response=CoinModel)
|
|
112
|
+
)
|
|
113
|
+
client.add(coins)
|
|
114
|
+
|
|
115
|
+
# 6. Make the request
|
|
116
|
+
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
|
|
117
|
+
print(coin.name) # "Bitcoin"
|
|
118
|
+
print(coin.symbol) # "btc"
|
|
119
|
+
|
|
120
|
+
asyncio.run(main())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Core Concepts
|
|
126
|
+
|
|
127
|
+
### Client
|
|
128
|
+
|
|
129
|
+
The `Client` is the entry point. It manages the HTTP connection, authentication, rate limiting, retries, and the resource tree.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client = xAPI.Client(
|
|
133
|
+
url="https://api.example.com/v1/",
|
|
134
|
+
apiKey=auth, # optional
|
|
135
|
+
timeout=httpx.Timeout(30, connect=5), # optional (default: 30s overall, 5s connect)
|
|
136
|
+
rateLimit=xAPI.RateLimiter(maxCalls=30, perSecond=60), # optional
|
|
137
|
+
retry=xAPI.RetryConfig(attempts=3, baseDelay=0.3, maxDelay=5.0), # optional
|
|
138
|
+
debug=True, # optional, enables logging
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Always close the client when done, either with `async with` or by calling `await client.close()`.
|
|
143
|
+
|
|
144
|
+
### Resource
|
|
145
|
+
|
|
146
|
+
A `Resource` is a named group of endpoints. Resources are registered on the client and accessed as attributes.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
coins = xAPI.Resource("coins")
|
|
150
|
+
client.add(coins)
|
|
151
|
+
|
|
152
|
+
# Now accessible as:
|
|
153
|
+
client.coins
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Path protection:** By default, the resource name is prepended to endpoint URLs. Set `pathProtection=False` to use the endpoint path as-is from the API root:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# With pathProtection=True (default):
|
|
160
|
+
# Endpoint path "list" -> request to /coins/list
|
|
161
|
+
coins = xAPI.Resource("coins")
|
|
162
|
+
|
|
163
|
+
# With pathProtection=False:
|
|
164
|
+
# Endpoint path "global" -> request to /global
|
|
165
|
+
markets = xAPI.Resource("markets", pathProtection=False)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Sub-resources** can be nested:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
parent = xAPI.Resource("api")
|
|
172
|
+
child = xAPI.Resource("coins")
|
|
173
|
+
parent.addResources(child)
|
|
174
|
+
# Access: client.api.coins.some_endpoint(...)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Authentication scoping:** Set `requireAuth=True` on a resource to enable per-endpoint authentication (when using `Scope.Endpoint`).
|
|
178
|
+
|
|
179
|
+
### Endpoint
|
|
180
|
+
|
|
181
|
+
An `Endpoint` represents a single API call. It defines the HTTP method, URL path, response model, and validation behavior.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
endpoint = xAPI.Endpoint(
|
|
185
|
+
name="coin", # Python attribute name
|
|
186
|
+
path=CoinPath(), # Path object with URL template
|
|
187
|
+
response=CoinModel, # Pydantic model or list[Model] for response parsing
|
|
188
|
+
method="GET", # HTTP method (default: "GET")
|
|
189
|
+
nameOverride="", # Override the API-facing name
|
|
190
|
+
strict=False, # Enable strict Pydantic validation
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Add endpoints to a resource:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
resource.addEndpoints(endpoint)
|
|
198
|
+
# or multiple:
|
|
199
|
+
resource.addEndpoints([endpoint1, endpoint2, endpoint3])
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Call an endpoint:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# Simple endpoint (no path parameters)
|
|
206
|
+
result = await client.coins.list()
|
|
207
|
+
|
|
208
|
+
# With path parameters
|
|
209
|
+
result = await client.coins.coin(parameters=CoinParams.Bitcoin)
|
|
210
|
+
|
|
211
|
+
# With query parameters
|
|
212
|
+
query = xAPI.Query({"localization": False, "tickers": False})
|
|
213
|
+
result = await client.coins.coin(parameters=CoinParams.Bitcoin, query=query)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Path & Parameters
|
|
217
|
+
|
|
218
|
+
Paths define URL templates. Parameters are typed enums that fill in the template placeholders.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# Define parameters as a StrEnum
|
|
222
|
+
class CoinParams(xAPI.Parameters):
|
|
223
|
+
Bitcoin = "bitcoin"
|
|
224
|
+
Ethereum = "ethereum"
|
|
225
|
+
Solana = "solana"
|
|
226
|
+
|
|
227
|
+
# Define a path with a placeholder
|
|
228
|
+
class CoinByID(xAPI.Path[CoinParams]):
|
|
229
|
+
endpointPath: str = "{id}"
|
|
230
|
+
|
|
231
|
+
# Path without parameters
|
|
232
|
+
class CoinList(xAPI.Path):
|
|
233
|
+
endpointPath: str = "list"
|
|
234
|
+
|
|
235
|
+
# Multi-segment path
|
|
236
|
+
class CoinTickers(xAPI.Path[CoinParams]):
|
|
237
|
+
endpointPath: str = "{id}/tickers"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Query
|
|
241
|
+
|
|
242
|
+
The `Query` class manages URL query parameters. Values set to `"NOT_GIVEN"` are automatically filtered out.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
query = xAPI.Query({
|
|
246
|
+
"vs_currency": "usd",
|
|
247
|
+
"order": "market_cap_desc",
|
|
248
|
+
"per_page": 100,
|
|
249
|
+
"sparkline": False,
|
|
250
|
+
"optional_param": "NOT_GIVEN", # filtered out
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
# Add more params
|
|
254
|
+
query.add({"page": 2})
|
|
255
|
+
|
|
256
|
+
# Remove a param
|
|
257
|
+
query.remove("sparkline")
|
|
258
|
+
|
|
259
|
+
# Inspect
|
|
260
|
+
print(query.queries) # dict of active params
|
|
261
|
+
print(query.queryString) # "vs_currency=usd&order=market_cap_desc&..."
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### ResponseModel
|
|
265
|
+
|
|
266
|
+
All response models should extend `xAPI.ResponseModel`, which extends Pydantic's `BaseModel` with convenience methods and optional API metadata.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
class Coin(xAPI.ResponseModel):
|
|
270
|
+
id: str
|
|
271
|
+
symbol: str
|
|
272
|
+
name: str
|
|
273
|
+
market_cap: float | None = None
|
|
274
|
+
current_price: float | None = None
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Convenience methods:**
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
|
|
281
|
+
|
|
282
|
+
# Convert to dict (excludes unset fields by default)
|
|
283
|
+
coin.toDict()
|
|
284
|
+
|
|
285
|
+
# Convert to formatted JSON string
|
|
286
|
+
coin.toJson(indent=2)
|
|
287
|
+
|
|
288
|
+
# Access API metadata (method, path, elapsed time)
|
|
289
|
+
print(coin.api.endpoint) # "GET coins/bitcoin in 0.35s"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**List responses:** When an API returns a JSON array instead of an object, use a `list[Model]` response type on the endpoint:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
class Category(xAPI.ResponseModel):
|
|
296
|
+
id: str
|
|
297
|
+
name: str
|
|
298
|
+
|
|
299
|
+
CategoryList = list[Category]
|
|
300
|
+
|
|
301
|
+
endpoint = xAPI.Endpoint(
|
|
302
|
+
name="categories",
|
|
303
|
+
path=CategoriesPath(),
|
|
304
|
+
response=CategoryList,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
categories = await client.coins.categories() # returns list[Category]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Authentication
|
|
313
|
+
|
|
314
|
+
xAPI supports API key authentication with three scoping levels:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
# Apply auth to ALL endpoints
|
|
318
|
+
auth = xAPI.APIKey(
|
|
319
|
+
keyName="x-api-key",
|
|
320
|
+
apiKey="your-secret-key",
|
|
321
|
+
scope=xAPI.Scope.All
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Apply auth only to endpoints on resources with requireAuth=True
|
|
325
|
+
auth = xAPI.APIKey(
|
|
326
|
+
keyName="x-api-key",
|
|
327
|
+
apiKey="your-secret-key",
|
|
328
|
+
scope=xAPI.Scope.Endpoint
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Disable auth
|
|
332
|
+
auth = xAPI.APIKey(
|
|
333
|
+
keyName="x-api-key",
|
|
334
|
+
apiKey="your-secret-key",
|
|
335
|
+
scope=xAPI.Scope.Disabled
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
When `scope=Scope.All`, the auth key-value pair is added to both request headers and query parameters on every request.
|
|
340
|
+
|
|
341
|
+
When `scope=Scope.Endpoint`, auth is only applied to requests made through resources that have `requireAuth=True`.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Rate Limiting
|
|
346
|
+
|
|
347
|
+
The built-in sliding window rate limiter prevents exceeding API quotas:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
rate_limiter = xAPI.RateLimiter(
|
|
351
|
+
maxCalls=30, # maximum number of calls
|
|
352
|
+
perSecond=60, # within this time window (seconds)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
client = xAPI.Client(
|
|
356
|
+
url="https://api.example.com/v1/",
|
|
357
|
+
rateLimit=rate_limiter,
|
|
358
|
+
)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The rate limiter uses an async lock and automatically pauses requests when the limit is reached.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Retry & Error Handling
|
|
366
|
+
|
|
367
|
+
### Retry Configuration
|
|
368
|
+
|
|
369
|
+
Retries use exponential backoff and only trigger on retriable errors (5xx, timeouts, connection errors). 4xx errors are raised immediately.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
retry = xAPI.RetryConfig(
|
|
373
|
+
attempts=3, # max retry attempts (default: 3)
|
|
374
|
+
baseDelay=0.3, # initial delay in seconds (default: 0.3)
|
|
375
|
+
maxDelay=5.0, # maximum delay in seconds (default: 5.0)
|
|
376
|
+
)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Exception Hierarchy
|
|
380
|
+
|
|
381
|
+
xAPI provides specific exception types for different failure modes:
|
|
382
|
+
|
|
383
|
+
| Exception | When |
|
|
384
|
+
|---|---|
|
|
385
|
+
| `APIStatusError` | Any 4xx or 5xx response |
|
|
386
|
+
| `BadRequestError` | HTTP 400 |
|
|
387
|
+
| `AuthenticationError` | HTTP 401 |
|
|
388
|
+
| `PermissionDeniedError` | HTTP 403 |
|
|
389
|
+
| `NotFoundError` | HTTP 404 |
|
|
390
|
+
| `ConflictError` | HTTP 409 |
|
|
391
|
+
| `UnprocessableEntityError` | HTTP 422 |
|
|
392
|
+
| `RateLimitError` | HTTP 429 |
|
|
393
|
+
| `InternalServerError` | HTTP 5xx |
|
|
394
|
+
| `APITimeoutError` | Request timed out |
|
|
395
|
+
| `APIConnectionError` | Connection failed |
|
|
396
|
+
| `APIResponseValidationError` | Response doesn't match the Pydantic model |
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
import xAPI
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
|
|
403
|
+
except xAPI.NotFoundError:
|
|
404
|
+
print("Coin not found")
|
|
405
|
+
except xAPI.RateLimitError:
|
|
406
|
+
print("Rate limited - slow down")
|
|
407
|
+
except xAPI.APIStatusError as e:
|
|
408
|
+
print(f"API error {e.status_code}: {e.message}")
|
|
409
|
+
except xAPI.APIConnectionError:
|
|
410
|
+
print("Could not connect to API")
|
|
411
|
+
except xAPI.APITimeoutError:
|
|
412
|
+
print("Request timed out")
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Nested Data Unwrapping
|
|
418
|
+
|
|
419
|
+
Many APIs wrap their response in a `{"data": {...}}` envelope. xAPI automatically unwraps this by default, so your models only need to define the inner data structure.
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
# API returns: {"data": {"total_market_cap": 2.5e12, "total_volume": 1e11}}
|
|
423
|
+
# Your model only needs:
|
|
424
|
+
class MarketData(xAPI.ResponseModel):
|
|
425
|
+
total_market_cap: float
|
|
426
|
+
total_volume: float
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
To disable this behavior, set `client.unsetNestedData = False`.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Options (Enum Helpers)
|
|
434
|
+
|
|
435
|
+
xAPI provides base enum classes for defining typed option values:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from xAPI import Options, IntOptions
|
|
439
|
+
|
|
440
|
+
# String-based options
|
|
441
|
+
Status = Options("Status", ["active", "inactive"])
|
|
442
|
+
Interval = Options("Interval", ["5m", "hourly", "daily"])
|
|
443
|
+
|
|
444
|
+
# Integer-based options
|
|
445
|
+
Days = IntOptions("Days", [("one", 1), ("seven", 7), ("thirty", 30)])
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Debug Logging
|
|
451
|
+
|
|
452
|
+
Enable debug logging to see detailed request/response information:
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
client = xAPI.Client(
|
|
456
|
+
url="https://api.example.com/v1/",
|
|
457
|
+
debug=True,
|
|
458
|
+
)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
This enables color-coded, structured logging for:
|
|
462
|
+
- Client operations (resource binding)
|
|
463
|
+
- HTTP requests (method, path, timing)
|
|
464
|
+
- Endpoint resolution
|
|
465
|
+
- Retry attempts
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Full Example
|
|
470
|
+
|
|
471
|
+
Here's a complete example using the CoinGecko API:
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
import asyncio
|
|
475
|
+
import xAPI
|
|
476
|
+
|
|
477
|
+
# --- Response Models ---
|
|
478
|
+
|
|
479
|
+
class Coin(xAPI.ResponseModel):
|
|
480
|
+
id: str
|
|
481
|
+
symbol: str
|
|
482
|
+
name: str
|
|
483
|
+
description: dict | None = None
|
|
484
|
+
market_data: dict | None = None
|
|
485
|
+
|
|
486
|
+
class CoinTickers(xAPI.ResponseModel):
|
|
487
|
+
name: str
|
|
488
|
+
tickers: list | None = None
|
|
489
|
+
|
|
490
|
+
class Category(xAPI.ResponseModel):
|
|
491
|
+
id: str
|
|
492
|
+
name: str
|
|
493
|
+
|
|
494
|
+
# --- Path Parameters ---
|
|
495
|
+
|
|
496
|
+
class CoinParams(xAPI.Parameters):
|
|
497
|
+
Bitcoin = "bitcoin"
|
|
498
|
+
Ethereum = "ethereum"
|
|
499
|
+
Solana = "solana"
|
|
500
|
+
|
|
501
|
+
# --- Paths ---
|
|
502
|
+
|
|
503
|
+
class CoinByID(xAPI.Path[CoinParams]):
|
|
504
|
+
endpointPath: str = "{id}"
|
|
505
|
+
|
|
506
|
+
class CoinTickersPath(xAPI.Path[CoinParams]):
|
|
507
|
+
endpointPath: str = "{id}/tickers"
|
|
508
|
+
|
|
509
|
+
class CategoriesPath(xAPI.Path):
|
|
510
|
+
endpointPath: str = "categories"
|
|
511
|
+
|
|
512
|
+
# --- Main ---
|
|
513
|
+
|
|
514
|
+
async def main():
|
|
515
|
+
auth = xAPI.APIKey(
|
|
516
|
+
keyName="x_cg_demo_api_key",
|
|
517
|
+
apiKey="YOUR_KEY",
|
|
518
|
+
scope=xAPI.Scope.All
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
async with xAPI.Client(
|
|
522
|
+
url="https://api.coingecko.com/api/v3/",
|
|
523
|
+
authentication=auth,
|
|
524
|
+
rateLimit=xAPI.RateLimiter(maxCalls=30, perSecond=60),
|
|
525
|
+
retry=xAPI.RetryConfig(attempts=3),
|
|
526
|
+
debug=True
|
|
527
|
+
) as client:
|
|
528
|
+
|
|
529
|
+
# Build the resource tree
|
|
530
|
+
coins = xAPI.Resource("coins")
|
|
531
|
+
coins.addEndpoints([
|
|
532
|
+
xAPI.Endpoint(name="coin", path=CoinByID(), response=Coin),
|
|
533
|
+
xAPI.Endpoint(name="tickers", path=CoinTickersPath(), response=CoinTickers),
|
|
534
|
+
xAPI.Endpoint(name="categories", path=CategoriesPath(), response=list[Category]),
|
|
535
|
+
])
|
|
536
|
+
client.add(coins)
|
|
537
|
+
|
|
538
|
+
# Fetch a coin with query parameters
|
|
539
|
+
query = xAPI.Query({
|
|
540
|
+
"localization": False,
|
|
541
|
+
"tickers": False,
|
|
542
|
+
"market_data": False,
|
|
543
|
+
"community_data": False,
|
|
544
|
+
"developer_data": False,
|
|
545
|
+
"sparkline": False,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
bitcoin = await client.coins.coin(
|
|
550
|
+
parameters=CoinParams.Bitcoin,
|
|
551
|
+
query=query
|
|
552
|
+
)
|
|
553
|
+
print(f"{bitcoin.name} ({bitcoin.symbol})")
|
|
554
|
+
print(bitcoin.toJson(indent=2))
|
|
555
|
+
|
|
556
|
+
# Fetch categories (list response)
|
|
557
|
+
categories = await client.coins.categories()
|
|
558
|
+
for cat in categories[:5]:
|
|
559
|
+
print(f" - {cat.name}")
|
|
560
|
+
|
|
561
|
+
except xAPI.NotFoundError:
|
|
562
|
+
print("Resource not found")
|
|
563
|
+
except xAPI.RateLimitError:
|
|
564
|
+
print("Rate limited")
|
|
565
|
+
except xAPI.APIStatusError as e:
|
|
566
|
+
print(f"API error: {e.status_code}")
|
|
567
|
+
|
|
568
|
+
asyncio.run(main())
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## API Reference
|
|
574
|
+
|
|
575
|
+
### `xAPI.Client(url, authentication?, timeout?, rateLimit?, retry?, headers?, debug?)`
|
|
576
|
+
|
|
577
|
+
The async HTTP client. Manages connections, auth, and the resource tree.
|
|
578
|
+
|
|
579
|
+
### `xAPI.Resource(name, prefix?, pathProtection?, requireAuth?)`
|
|
580
|
+
|
|
581
|
+
A named group of endpoints. Add to client with `client.add(resource)`.
|
|
582
|
+
|
|
583
|
+
### `xAPI.Endpoint(name, path, response?, method?, nameOverride?, strict?)`
|
|
584
|
+
|
|
585
|
+
A single API endpoint definition.
|
|
586
|
+
|
|
587
|
+
### `xAPI.Path[P]`
|
|
588
|
+
|
|
589
|
+
Protocol for URL path templates. Subclass and set `endpointPath`.
|
|
590
|
+
|
|
591
|
+
### `xAPI.Parameters`
|
|
592
|
+
|
|
593
|
+
Base `StrEnum` for typed path parameters.
|
|
594
|
+
|
|
595
|
+
### `xAPI.Query(queries)`
|
|
596
|
+
|
|
597
|
+
Query parameter builder. Filters out `"NOT_GIVEN"` values.
|
|
598
|
+
|
|
599
|
+
### `xAPI.APIKey(keyName, apiKey, scope)`
|
|
600
|
+
|
|
601
|
+
API key authentication with configurable scope.
|
|
602
|
+
|
|
603
|
+
### `xAPI.RateLimiter(maxCalls, perSecond)`
|
|
604
|
+
|
|
605
|
+
Sliding window rate limiter.
|
|
606
|
+
|
|
607
|
+
### `xAPI.RetryConfig(attempts?, baseDelay?, maxDelay?)`
|
|
608
|
+
|
|
609
|
+
Exponential backoff retry configuration.
|
|
610
|
+
|
|
611
|
+
### `xAPI.ResponseModel`
|
|
612
|
+
|
|
613
|
+
Base model for API responses. Extends Pydantic `BaseModel` with `toDict()` and `toJson()`.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
## 📚 ・ xDev Utilities
|
|
617
|
+
This library is part of **xDev Utilities**. As set of power tool to streamline your workflow.
|
|
618
|
+
|
|
619
|
+
- **[xAPI](https://github.com/rkohl/xAPI)**: A lightweight, flexible asynchronous API client for Python built on Pydantic and httpx
|
|
620
|
+
- **[xEvents](https://github.com/rkohl/xEvents)**: A lightweight, thread-safe event system for Python
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
## License
|
|
624
|
+
|
|
625
|
+
BSD-3-Clause
|