Universal, lightweight HTTP API wrapper with unified response handling, structured error diagnostics, and configurable JSON decoding strategies.
Project description
ouroboros_api
A universal, lightweight HTTP API wrapper with unified response handling, structured error diagnostics, and configurable JSON decoding strategies.
Features
- Unified response object — every call returns
APIResponse, no raw exceptions leak - Intelligent JSON handling —
expect_json=True | False | "auto" - Full HTTP verb coverage — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- Generic
request()method — for arbitrary HTTP verbs (LOCK, PROPFIND, etc.) - Retry with backoff —
request_with_retry()for transient failure recovery - File download — streaming
download_file()with chunk-based I/O - Health check — lightweight
health_check()for endpoint availability - Auth helpers — Bearer, Basic, API Key header builders
- URL builder — safe path join with slash normalization
- Structured errors —
APIErrorDetailwith UTC timestamps - Exception hierarchy — typed exceptions with
raise_for_error()integration - Fully typed — PEP 561
py.typed, type aliases, NumPy-style docstrings
Installation
pip install ouroboros_api
Or install from source:
pip install -e .
With test dependencies:
pip install -e ".[test]"
Quick Start
from ouroboros_api import BaseAPI
resp = BaseAPI.make_get_request("https://api.example.com/v1/users")
if resp: # APIResponse supports bool() — equivalent to resp.ok
print("Users:", resp.data)
else:
print("Error:", resp.error)
Architecture
ouroboros_api/
├── __init__.py # Public API exports
├── base.py # BaseAPI facade (composes AuthMixin + VerbMixin)
├── _types.py # Type aliases (JSONMode, Headers, Cookies, etc.)
├── _response.py # APIResponse + APIErrorDetail
├── _auth.py # AuthMixin: Bearer, Basic, API Key, URL join
├── _client.py # ClientMixin: request dispatcher, retry, headers
├── _verbs.py # VerbMixin: HTTP verb methods + download + health
├── exceptions.py # Exception hierarchy
├── py.typed # PEP 561 typed marker
└── tests/ # Unit tests (106 tests)
API Reference — BaseAPI
All methods are @staticmethod — no instance required.
Authentication Helpers
| Method | Description | Returns |
|---|---|---|
build_bearer_token(token) |
Format Authorization: Bearer header value |
str |
build_basic_auth(username, password) |
Base64-encode Authorization: Basic header value |
str |
build_api_key_header(key, header_name="X-API-Key") |
Build a single-entry header dict for API key auth | dict[str, str] |
join_url(base, *parts) |
Safely join base URL with path segments (slash-safe) | str |
# Bearer token
token = BaseAPI.build_bearer_token("eyJhbGci...")
resp = BaseAPI.make_get_request(url, auth_token=token)
# Basic auth
auth = BaseAPI.build_basic_auth("admin", "secret")
resp = BaseAPI.make_get_request(url, auth_token=auth)
# API key
headers = BaseAPI.build_api_key_header("my-key-123")
resp = BaseAPI.make_get_request(url, headers_param=headers)
# URL builder
url = BaseAPI.join_url("https://api.example.com/v1", "users", "42")
# → "https://api.example.com/v1/users/42"
HTTP Verb Methods
| Method | Default Success Statuses | Notes |
|---|---|---|
make_get_request(url, ...) |
200, 201, 202, 203, 204, 304 | Standard GET |
make_post_request(url, ...) |
200, 201, 202, 204 | Supports payload, data, files |
make_put_request(url, ...) |
200, 201, 202, 204 | Full resource replacement |
make_patch_request(url, ...) |
200, 201, 202, 204 | Partial resource update |
make_delete_request(url, ...) |
200, 202, 204 | Resource deletion |
make_head_request(url, ...) |
200, 204, 301, 302, 304 | Metadata only, no body |
make_options_request(url, ...) |
200, 204 | CORS pre-flight / allowed methods |
Common parameters (all verb methods):
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
str |
— | Fully-qualified request URL |
auth_token |
str |
"" |
Authorization header value |
headers_param |
Mapping[str, str] | None |
None |
Additional headers |
cookies_param |
dict[str, str] | None |
None |
Request cookies |
params |
dict[str, Any] | None |
None |
Query-string parameters |
expect_json |
bool | "auto" |
True |
JSON decoding strategy |
success_statuses |
Sequence[int] | None |
varies | Custom success codes |
timeout |
float | tuple[float, float] |
10 |
Timeout in seconds |
verify |
bool |
True |
SSL certificate verification |
Body parameters (POST, PUT, PATCH only):
| Parameter | Type | Description |
|---|---|---|
payload |
Any |
JSON-serializable body (sets Content-Type: application/json) |
data |
Any |
Raw or form-encoded body |
files |
Any |
Multipart file-upload mapping |
Precedence: When multiple body params are supplied:
files > data > payload.
Generic Request
# Arbitrary HTTP verbs
resp = BaseAPI.request("LOCK", "https://dav.example.com/file.txt")
# All keyword args from verb methods are available
resp = BaseAPI.request("POST", url, payload={"key": "value"}, timeout=30)
Retry
resp = BaseAPI.request_with_retry(
"GET",
"https://api.example.com/v1/data",
retries=3, # max attempts (default: 3)
retry_delay=1.0, # seconds between retries (default: 1.0)
retry_on_statuses=(408, 429, 500, 502, 503, 504), # default
)
File Download
resp = BaseAPI.download_file(
"https://cdn.example.com/report.pdf",
"/tmp/report.pdf",
auth_token=token,
chunk_size=8192, # bytes per chunk (default: 8192)
timeout=60, # default: 60
)
if resp.ok:
print(f"Saved to: {resp.data}") # data contains local file path
Health Check
if BaseAPI.health_check("https://api.example.com/health"):
print("Service is up")
else:
print("Service is down")
APIResponse
Every HTTP call returns an APIResponse dataclass.
Fields
| Field | Type | Description |
|---|---|---|
ok |
bool |
True if status is in success codes and body satisfies expect_json |
status |
int |
HTTP status code (0 when request could not be sent) |
data |
Any |
Parsed JSON, raw bytes, or None on error |
error |
str | None |
Multi-line error report (None on success) |
raw_text |
str |
Full response body as text |
raw_bytes |
bytes |
Original binary payload |
headers |
dict[str, str] |
Response headers |
elapsed |
float | None |
Wall-clock request time in seconds |
Properties & Methods
| Name | Returns | Description |
|---|---|---|
bool(resp) |
bool |
Shorthand for resp.ok |
resp.is_json |
bool |
True if data is dict or list |
resp.json_data |
dict | list | None |
Returns data if JSON, else None |
resp.content_type |
str |
Content-Type header (empty if missing) |
resp.content_length |
int |
Content-Length header (0 if missing) |
resp.raise_for_error() |
None |
Raises typed exception if ok is False |
JSON Decoding Strategies
expect_json |
Behaviour |
|---|---|
True (default) |
JSON mandatory — non-JSON body is treated as an error |
False |
Raw bytes returned in data — no JSON parsing |
"auto" |
Try JSON first, silently fall back to raw bytes |
# Strict JSON (default)
resp = BaseAPI.make_get_request(url)
# Raw binary download
resp = BaseAPI.make_get_request(url, expect_json=False)
print(type(resp.data)) # <class 'bytes'>
# Auto-detect
resp = BaseAPI.make_delete_request(url, expect_json="auto")
if resp.is_json:
print(resp.json_data)
Error Handling
Pattern 1: Check .ok
resp = BaseAPI.make_post_request(url, payload={"key": "value"})
if not resp.ok:
print(resp.error) # multi-line formatted error report
Pattern 2: Raise Exceptions
from ouroboros_api import APIResponseError, APITimeoutError, APIConnectionError
try:
resp = BaseAPI.make_post_request(url, payload={"key": "value"})
resp.raise_for_error()
except APITimeoutError:
print("Request timed out")
except APIConnectionError:
print("Cannot reach server")
except APIResponseError as e:
print(f"API error (HTTP {e.status_code}): {e}")
Exception Hierarchy
APIError
├── APIRequestError ← network / transport failures
│ ├── APITimeoutError ← connect or read timeout
│ └── APIConnectionError ← DNS / TCP / TLS failures
└── APIResponseError ← HTTP response received but invalid / unexpected
All exceptions have a status_code attribute (except APIError base).
Type Aliases
Importable from ouroboros_api for use in your own type hints:
| Alias | Definition | Description |
|---|---|---|
JSONMode |
bool | Literal["auto"] |
JSON decoding strategy |
Headers |
Mapping[str, str] | None |
HTTP headers |
Cookies |
dict[str, str] | None |
Cookie jar |
QueryParams |
dict[str, Any] | None |
Query parameters |
TimeoutType |
float | tuple[float, float] |
Timeout value |
SuccessStatuses |
Sequence[int] | None |
Success status codes |
from ouroboros_api import Headers, TimeoutType
def fetch(url: str, headers: Headers = None, timeout: TimeoutType = 10):
return BaseAPI.make_get_request(url, headers_param=headers, timeout=timeout)
Timeout
# Single timeout (connect + read)
resp = BaseAPI.make_get_request(url, timeout=30)
# Tuple timeout (connect, read) — independent control
resp = BaseAPI.make_get_request(url, timeout=(5, 30))
Multipart Upload
resp = BaseAPI.make_post_request(
"https://api.example.com/upload",
files={"file": open("report.zip", "rb")},
)
Content-Typeis automatically set byrequestswith the multipart boundary.
Running Tests
pip install -e ".[test]"
python -m pytest tests/ -v
License
This project is licensed under the MIT License - see the LICENSE file for details.
Authors
Flavio Brandolini
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ouroboros_api-1.0.0.tar.gz.
File metadata
- Download URL: ouroboros_api-1.0.0.tar.gz
- Upload date:
- Size: 25.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c95f934801e89207a55819b6cf7b744c381d7d80d22d235d46cf56c7d92cf21
|
|
| MD5 |
306e725f71311baed9c77194183969dd
|
|
| BLAKE2b-256 |
5d2bf67898f3abe5339bf120c46492f780fd9d646d418c2da5eae47ed76d218e
|
File details
Details for the file ouroboros_api-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ouroboros_api-1.0.0-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eea918b21c94b40263331afcdaee0e8abbc2664202b9667ee6a83c743ca53e24
|
|
| MD5 |
7ba5da4c176d151e1ccee20269bcdfad
|
|
| BLAKE2b-256 |
d604c672e90be281b381e3c7f2d6a93f65d09c1f50e509de2358091428b54c3f
|