A lightweight, flexible asynchronous API client for Python built on httpx and pydantic.
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.
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
print(coin.name) # "Bitcoin"- Async-first β Built on
httpx.AsyncClientfor high-performance, non-blocking requests. - Resource-oriented β Organize endpoints into a hierarchical tree. Access them with dot notation (
client.coins.coin(...)). - Type-safe responses β Pydantic models validate and parse every API response automatically.
- List responses β Handle endpoints that return JSON arrays by passing a
list[Model]response type. - Parameterized paths β Define URL templates like
{id}and inject values with type-safeParametersenums. - Query parameters β Build and manage query strings with the
Queryclass. - Authentication β Scoped API key auth β apply globally, per-endpoint, or disable entirely.
- Rate limiting β Built-in sliding window rate limiter to stay within API quotas.
- Retry with backoff β Automatic exponential backoff on 5xx errors, timeouts, and connection failures. 4xx errors are raised immediately.
- Structured logging β Color-coded, per-component logging via
loguru(enabled withdebug=True). - Context manager β Proper connection cleanup with
async withsupport.
NOTE: This is an experimental project and proof of concept. May not fully work as indended.
Install xAPI using pip
$ pip install xAPI-clientRequirements: Python 3.12+
import asyncio
import xAPI
from pydantic import BaseModel
# 1. Define a response model
class CoinModel(xAPI.ResponseModel):
id: str
symbol: str
name: str
# 2. Define path parameters
class CoinParams(xAPI.Parameters):
Bitcoin = "bitcoin"
Ethereum = "ethereum"
# 3. Define a path with parameter placeholders
class CoinPath(xAPI.Path[CoinParams]):
endpointPath: str = "{id}"
async def main():
# 4. Create the client
auth = xAPI.APIKey(
key="x_cg_demo_api_key",
secret="YOUR_API_KEY",
scope="All",
schem="Header"
)
async with xAPI.Client(
url="https://api.example.com/v1/",
apiKey=auth
) as client:
# 5. Build resources and endpoints
coins = xAPI.Resource("coins")
coins.addEndpoints(
xAPI.Endpoint(name="coin", path=CoinPath(), response=CoinModel)
)
client.add(coins)
# 6. Make the request
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
print(coin.name) # "Bitcoin"
print(coin.symbol) # "btc"
asyncio.run(main())The Client is the entry point. It manages the HTTP connection, authentication, rate limiting, retries, and the resource tree.
client = xAPI.Client(
url="https://api.example.com/v1/",
apiKey=auth, # optional
timeout=httpx.Timeout(30, connect=5), # optional (default: 30s overall, 5s connect)
rateLimit=xAPI.RateLimiter(maxCalls=30, perSecond=60), # optional
retry=xAPI.RetryConfig(attempts=3, baseDelay=0.3, maxDelay=5.0), # optional
debug=True, # optional, enables logging
)Always close the client when done, either with async with or by calling await client.close().
A Resource is a named group of endpoints. Resources are registered on the client and accessed as attributes.
coins = xAPI.Resource("coins")
client.add(coins)
# Now accessible as:
client.coinsPath 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:
# With pathProtection=True (default):
# Endpoint path "list" -> request to /coins/list
coins = xAPI.Resource("coins")
# With pathProtection=False:
# Endpoint path "global" -> request to /global
markets = xAPI.Resource("markets", pathProtection=False)Sub-resources can be nested:
parent = xAPI.Resource("api")
child = xAPI.Resource("coins")
parent.addResources(child)
# Access: client.api.coins.some_endpoint(...)Authentication scoping: Set requireAuth=True on a resource to enable per-endpoint authentication (when using Scope.Endpoint).
An Endpoint represents a single API call. It defines the HTTP method, URL path, response model, and validation behavior.
endpoint = xAPI.Endpoint(
name="coin", # Python attribute name
path=CoinPath(), # Path object with URL template
response=CoinModel, # Pydantic model or list[Model] for response parsing
method="GET", # HTTP method (default: "GET")
nameOverride="", # Override the API-facing name
strict=False, # Enable strict Pydantic validation
)Add endpoints to a resource:
resource.addEndpoints(endpoint)
# or multiple:
resource.addEndpoints([endpoint1, endpoint2, endpoint3])Call an endpoint:
# Simple endpoint (no path parameters)
result = await client.coins.list()
# With path parameters
result = await client.coins.coin(parameters=CoinParams.Bitcoin)
# With query parameters
query = xAPI.Query({"localization": False, "tickers": False})
result = await client.coins.coin(parameters=CoinParams.Bitcoin, query=query)Paths define URL templates. Parameters are typed enums that fill in the template placeholders.
# Define parameters as a StrEnum
class CoinParams(xAPI.Parameters):
Bitcoin = "bitcoin"
Ethereum = "ethereum"
Solana = "solana"
# Define a path with a placeholder
class CoinByID(xAPI.Path[CoinParams]):
endpointPath: str = "{id}"
# Path without parameters
class CoinList(xAPI.Path):
endpointPath: str = "list"
# Multi-segment path
class CoinTickers(xAPI.Path[CoinParams]):
endpointPath: str = "{id}/tickers"The Query class manages URL query parameters. Values set to "NOT_GIVEN" are automatically filtered out.
query = xAPI.Query({
"vs_currency": "usd",
"order": "market_cap_desc",
"per_page": 100,
"sparkline": False,
"optional_param": "NOT_GIVEN", # filtered out
})
# Add more params
query.add({"page": 2})
# Remove a param
query.remove("sparkline")
# Inspect
print(query.queries) # dict of active params
print(query.queryString) # "vs_currency=usd&order=market_cap_desc&..."All response models should extend xAPI.ResponseModel, which extends Pydantic's BaseModel with convenience methods and optional API metadata.
class Coin(xAPI.ResponseModel):
id: str
symbol: str
name: str
market_cap: float | None = None
current_price: float | None = NoneConvenience methods:
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
# Convert to dict (excludes unset fields by default)
coin.toDict()
# Convert to formatted JSON string
coin.toJson(indent=2)
# Access API metadata (method, path, elapsed time)
print(coin.api.endpoint) # "GET coins/bitcoin in 0.35s"List responses: When an API returns a JSON array instead of an object, use a list[Model] response type on the endpoint:
class Category(xAPI.ResponseModel):
id: str
name: str
CategoryList = list[Category]
endpoint = xAPI.Endpoint(
name="categories",
path=CategoriesPath(),
response=CategoryList,
)
categories = await client.coins.categories() # returns list[Category]xAPI supports API key authentication with three scoping levels:
# Apply auth to ALL endpoints
auth = xAPI.APIKey(
keyName="x-api-key",
apiKey="your-secret-key",
scope=xAPI.Scope.All
)
# Apply auth only to endpoints on resources with requireAuth=True
auth = xAPI.APIKey(
keyName="x-api-key",
apiKey="your-secret-key",
scope=xAPI.Scope.Endpoint
)
# Disable auth
auth = xAPI.APIKey(
keyName="x-api-key",
apiKey="your-secret-key",
scope=xAPI.Scope.Disabled
)When scope=Scope.All, the auth key-value pair is added to both request headers and query parameters on every request.
When scope=Scope.Endpoint, auth is only applied to requests made through resources that have requireAuth=True.
The built-in sliding window rate limiter prevents exceeding API quotas:
rate_limiter = xAPI.RateLimiter(
maxCalls=30, # maximum number of calls
perSecond=60, # within this time window (seconds)
)
client = xAPI.Client(
url="https://api.example.com/v1/",
rateLimit=rate_limiter,
)The rate limiter uses an async lock and automatically pauses requests when the limit is reached.
Retries use exponential backoff and only trigger on retriable errors (5xx, timeouts, connection errors). 4xx errors are raised immediately.
retry = xAPI.RetryConfig(
attempts=3, # max retry attempts (default: 3)
baseDelay=0.3, # initial delay in seconds (default: 0.3)
maxDelay=5.0, # maximum delay in seconds (default: 5.0)
)xAPI provides specific exception types for different failure modes:
| Exception | When |
|---|---|
APIStatusError |
Any 4xx or 5xx response |
BadRequestError |
HTTP 400 |
AuthenticationError |
HTTP 401 |
PermissionDeniedError |
HTTP 403 |
NotFoundError |
HTTP 404 |
ConflictError |
HTTP 409 |
UnprocessableEntityError |
HTTP 422 |
RateLimitError |
HTTP 429 |
InternalServerError |
HTTP 5xx |
APITimeoutError |
Request timed out |
APIConnectionError |
Connection failed |
APIResponseValidationError |
Response doesn't match the Pydantic model |
import xAPI
try:
coin = await client.coins.coin(parameters=CoinParams.Bitcoin)
except xAPI.NotFoundError:
print("Coin not found")
except xAPI.RateLimitError:
print("Rate limited - slow down")
except xAPI.APIStatusError as e:
print(f"API error {e.status_code}: {e.message}")
except xAPI.APIConnectionError:
print("Could not connect to API")
except xAPI.APITimeoutError:
print("Request timed out")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.
# API returns: {"data": {"total_market_cap": 2.5e12, "total_volume": 1e11}}
# Your model only needs:
class MarketData(xAPI.ResponseModel):
total_market_cap: float
total_volume: floatTo disable this behavior, set client.unsetNestedData = False.
xAPI provides base enum classes for defining typed option values:
from xAPI import Options, IntOptions
# String-based options
Status = Options("Status", ["active", "inactive"])
Interval = Options("Interval", ["5m", "hourly", "daily"])
# Integer-based options
Days = IntOptions("Days", [("one", 1), ("seven", 7), ("thirty", 30)])Enable debug logging to see detailed request/response information:
client = xAPI.Client(
url="https://api.example.com/v1/",
debug=True,
)This enables color-coded, structured logging for:
- Client operations (resource binding)
- HTTP requests (method, path, timing)
- Endpoint resolution
- Retry attempts
Here's a complete example using the CoinGecko API:
import asyncio
import xAPI
# --- Response Models ---
class Coin(xAPI.ResponseModel):
id: str
symbol: str
name: str
description: dict | None = None
market_data: dict | None = None
class CoinTickers(xAPI.ResponseModel):
name: str
tickers: list | None = None
class Category(xAPI.ResponseModel):
id: str
name: str
# --- Path Parameters ---
class CoinParams(xAPI.Parameters):
Bitcoin = "bitcoin"
Ethereum = "ethereum"
Solana = "solana"
# --- Paths ---
class CoinByID(xAPI.Path[CoinParams]):
endpointPath: str = "{id}"
class CoinTickersPath(xAPI.Path[CoinParams]):
endpointPath: str = "{id}/tickers"
class CategoriesPath(xAPI.Path):
endpointPath: str = "categories"
# --- Main ---
async def main():
auth = xAPI.APIKey(
keyName="x_cg_demo_api_key",
apiKey="YOUR_KEY",
scope=xAPI.Scope.All
)
async with xAPI.Client(
url="https://api.coingecko.com/api/v3/",
authentication=auth,
rateLimit=xAPI.RateLimiter(maxCalls=30, perSecond=60),
retry=xAPI.RetryConfig(attempts=3),
debug=True
) as client:
# Build the resource tree
coins = xAPI.Resource("coins")
coins.addEndpoints([
xAPI.Endpoint(name="coin", path=CoinByID(), response=Coin),
xAPI.Endpoint(name="tickers", path=CoinTickersPath(), response=CoinTickers),
xAPI.Endpoint(name="categories", path=CategoriesPath(), response=list[Category]),
])
client.add(coins)
# Fetch a coin with query parameters
query = xAPI.Query({
"localization": False,
"tickers": False,
"market_data": False,
"community_data": False,
"developer_data": False,
"sparkline": False,
})
try:
bitcoin = await client.coins.coin(
parameters=CoinParams.Bitcoin,
query=query
)
print(f"{bitcoin.name} ({bitcoin.symbol})")
print(bitcoin.toJson(indent=2))
# Fetch categories (list response)
categories = await client.coins.categories()
for cat in categories[:5]:
print(f" - {cat.name}")
except xAPI.NotFoundError:
print("Resource not found")
except xAPI.RateLimitError:
print("Rate limited")
except xAPI.APIStatusError as e:
print(f"API error: {e.status_code}")
asyncio.run(main())The async HTTP client. Manages connections, auth, and the resource tree.
A named group of endpoints. Add to client with client.add(resource).
A single API endpoint definition.
Protocol for URL path templates. Subclass and set endpointPath.
Base StrEnum for typed path parameters.
Query parameter builder. Filters out "NOT_GIVEN" values.
API key authentication with configurable scope.
Sliding window rate limiter.
Exponential backoff retry configuration.
Base model for API responses. Extends Pydantic BaseModel with toDict() and toJson().
This library is part of xDev Utilities. As set of power tool to streamline your workflow.
- xAPI: A lightweight, flexible asynchronous API client for Python built on Pydantic and httpx
- xEvents: A lightweight, thread-safe event system for Python
BSD-3-Clause