A type-safe HTTP client library with validation and detailed error reporting.
Project description
Typed Requests
A modern, type-safe HTTP client for Python 3.13+ that validates API responses at runtime. Built on httpx with complete type validation, it catches data issues before they become bugs.
🎯 Why Typed Requests?
The Problem
Working with external APIs in Python is risky:
- API responses don't always match documentation 📋❌
- Silent type mismatches cause bugs deep in your code 🐛
- No runtime validation of response structures 🏗️
- Poor error messages when things go wrong 😕
The Solution
Typed Requests provides automatic runtime validation for all your HTTP requests:
# Without Typed Requests - Hope the API is correct 🤞
response = requests.get("https://api.example.com/user/123")
user_data = response.json()
# This might crash anywhere in your code:
print(f"User {user_data['name']} has {user_data['credits']} credits")
# With Typed Requests - Validated and type-safe ✅
from typed_requests import networking_manager
from typing import TypedDict
class User(TypedDict):
name: str
credits: int
response = await networking_manager.get(
"https://api.example.com/user/123",
expected_type=User
)
# Safe to use - guaranteed to match User structure
print(f"User {response.data['name']} has {response.data['credits']} credits")
✨ Features
- 🛡️ Runtime Type Validation: Catch API changes and inconsistencies immediately
- 🔍 Clear Error Messages: Know exactly what's wrong:
data.users[0].email: Expected str, got null - 🚀 Built on httpx: Modern async HTTP with HTTP/2 support
- 🎯 Type-Safe Returns: Get
TypedResponse[T]with full IDE autocomplete - 🔄 Smart Type Conversion: Automatically converts compatible types (dict → dataclass)
- ⚡ High Performance: Connection pooling and persistent sessions
- 🐍 Pure Python Types: Works with TypedDict, dataclass, Optional, Union, and more
- ✅ 100% Test Coverage: Battle-tested and production-ready
📦 Installation
# Using pip
pip install divine-typed-requests
# Using uv
uv add divine-typed-requests
# For development
git clone https://github.com/divine/typed-requests
cd typed-requests
uv sync
Requirements
- Python 3.13+
- httpx
- divine-type-enforcer
🚀 Quick Start
import asyncio
from typed_requests import networking_manager
from typing import TypedDict, List
class Post(TypedDict):
id: int
title: str
content: str
author: str
likes: int
class PostsResponse(TypedDict):
posts: List[Post]
total: int
page: int
async def main():
# Fetch and validate blog posts
response = await networking_manager.get(
"https://api.example.com/posts",
expected_type=PostsResponse
)
# response.data is fully typed and validated
for post in response.data['posts']:
print(f"{post['title']} by {post['author']} ({post['likes']} likes)")
print(f"Showing page {response.data['page']} of {response.data['total']} posts")
asyncio.run(main())
📖 Usage Examples
Basic Requests (Untyped)
For simple requests without validation:
from typed_requests import networking_manager
# Simple GET request
response = await networking_manager.get("https://api.github.com/users/octocat")
print(response.json()["name"]) # "The Octocat"
# POST with JSON data
response = await networking_manager.post(
"https://api.example.com/users",
json={"name": "Alice", "email": "alice@example.com"}
)
print(response.status_code) # 201
Type-Safe Requests
Add type validation to catch issues early:
from typing import TypedDict, Optional, List
from typed_requests import networking_manager
from type_enforcer import ValidationError
class GitHubUser(TypedDict):
login: str
id: int
name: Optional[str]
email: Optional[str]
bio: Optional[str]
public_repos: int
followers: int
following: int
try:
# Fetch with type validation
response = await networking_manager.get(
"https://api.github.com/users/octocat",
expected_type=GitHubUser
)
# response.data is now typed as GitHubUser
user = response.data
print(f"{user['login']} has {user['followers']} followers")
except ValidationError as e:
print(f"API response doesn't match expected structure: {e}")
Working with Complex APIs
from typing import TypedDict, List, Optional, Literal
from datetime import datetime
class Repository(TypedDict):
id: int
name: str
full_name: str
description: Optional[str]
private: bool
fork: bool
created_at: str
updated_at: str
pushed_at: str
language: Optional[str]
stargazers_count: int
watchers_count: int
forks_count: int
class SearchResponse(TypedDict):
total_count: int
incomplete_results: bool
items: List[Repository]
# Search GitHub repositories with full type safety
response = await networking_manager.get(
"https://api.github.com/search/repositories",
params={"q": "language:python stars:>1000", "sort": "stars"},
expected_type=SearchResponse
)
# Process validated results
for repo in response.data['items'][:5]:
print(f"⭐ {repo['stargazers_count']:,} - {repo['full_name']}")
if repo['description']:
print(f" {repo['description'][:60]}...")
Error Handling
from typed_requests import networking_manager
from type_enforcer import ValidationError
import httpx
class ExpectedResponse(TypedDict):
status: str
data: dict[str, any]
try:
response = await networking_manager.get(
"https://api.example.com/data",
expected_type=ExpectedResponse,
timeout=30.0
)
print(f"Status: {response.data['status']}")
except ValidationError as e:
# Handle validation errors
print(f"Response validation failed: {e}")
# Example: "data.status: Expected str, got int"
except httpx.HTTPStatusError as e:
# Handle HTTP errors (4xx, 5xx)
print(f"HTTP error {e.response.status_code}: {e.response.text}")
except httpx.RequestError as e:
# Handle connection errors
print(f"Connection error: {e}")
All HTTP Methods
from typed_requests import networking_manager
# GET request
user = await networking_manager.get(
"https://api.example.com/users/123",
expected_type=UserResponse
)
# POST request
new_user = await networking_manager.post(
"https://api.example.com/users",
json={"name": "Bob", "email": "bob@example.com"},
expected_type=UserResponse
)
# PUT request
updated_user = await networking_manager.put(
"https://api.example.com/users/123",
json={"name": "Robert", "email": "robert@example.com"},
expected_type=UserResponse
)
# PATCH request
patched_user = await networking_manager.patch(
"https://api.example.com/users/123",
json={"name": "Rob"},
expected_type=UserResponse
)
# DELETE request
await networking_manager.delete("https://api.example.com/users/123")
# HEAD request
headers = await networking_manager.head("https://api.example.com/users/123")
# OPTIONS request
options = await networking_manager.options("https://api.example.com/users")
Custom Headers and Authentication
# Bearer token authentication
response = await networking_manager.get(
"https://api.example.com/protected",
headers={"Authorization": "Bearer your-token-here"},
expected_type=ProtectedResource
)
# Custom headers
response = await networking_manager.post(
"https://api.example.com/data",
json={"key": "value"},
headers={
"X-API-Key": "your-api-key",
"X-Client-Version": "1.0.0"
},
expected_type=ApiResponse
)
Advanced Configuration
from typed_requests import NetworkingManager
from typed_requests.tls import TLS_CONTEXT_HTTP2
# Create custom manager with specific configuration
async def create_custom_client():
manager = NetworkingManager(
tls_context=TLS_CONTEXT_HTTP2,
enable_http2=True
)
await manager.startup()
try:
# Use the manager
response = await manager.get(
"https://api.example.com/data",
timeout=60.0, # 60 second timeout
expected_type=DataResponse
)
return response.data
finally:
# Always cleanup
await manager.shutdown()
# Run with proper lifecycle management
data = await create_custom_client()
🏗️ Real-World Example: Financial API Integration
Here's a complete example using the DexScreener API:
from typing import TypedDict, List, Optional
from typed_requests import networking_manager
from decimal import Decimal
class Token(TypedDict):
address: str
name: str
symbol: str
class Liquidity(TypedDict):
usd: float
base: float
quote: float
class Volume(TypedDict):
h24: float
h6: float
h1: float
m5: float
class PriceChange(TypedDict):
h24: float
h6: float
h1: float
m5: float
class TokenPair(TypedDict):
chainId: str
dexId: str
url: str
pairAddress: str
labels: Optional[List[str]]
baseToken: Token
quoteToken: Token
priceNative: str
priceUsd: str
liquidity: Liquidity
fdv: float
marketCap: float
pairCreatedAt: int
volume: Volume
priceChange: PriceChange
trades: dict[str, int]
quoteTokenSymbol: str
class DexScreenerResponse(TypedDict):
schemaVersion: str
pairs: List[TokenPair]
async def get_trending_tokens():
"""Fetch and analyze trending tokens from DexScreener"""
response = await networking_manager.get(
"https://api.dexscreener.com/latest/dex/tokens/SOL",
expected_type=DexScreenerResponse
)
# Process validated data safely
pairs = response.data['pairs']
# Sort by 24h volume
sorted_pairs = sorted(
pairs,
key=lambda p: p['volume']['h24'],
reverse=True
)
print("🔥 Top Trending SOL Tokens by Volume:\n")
for pair in sorted_pairs[:10]:
symbol = pair['baseToken']['symbol']
price = float(pair['priceUsd'])
volume_24h = pair['volume']['h24']
change_24h = pair['priceChange']['h24']
liquidity = pair['liquidity']['usd']
# Emoji based on price change
trend = "🟢" if change_24h > 0 else "🔴"
print(f"{trend} {symbol}")
print(f" Price: ${price:.6f}")
print(f" 24h Volume: ${volume_24h:,.0f}")
print(f" 24h Change: {change_24h:+.2f}%")
print(f" Liquidity: ${liquidity:,.0f}")
print(f" DEX: {pair['dexId']}")
print()
# Run the analysis
await get_trending_tokens()
📚 API Reference
NetworkingManager
The main class for making HTTP requests.
Methods
All methods support the same parameters:
async def get(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def post(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def put(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def patch(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def delete(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def head(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
async def options(url: str, *, expected_type: Optional[Type[T]] = None, **kwargs) -> Union[Response, TypedResponse[T]]
Parameters
| Parameter | Type | Description |
|---|---|---|
url |
str |
The URL to request |
expected_type |
Optional[Type[T]] |
Expected response type for validation |
**kwargs |
Any |
Additional arguments passed to httpx |
Common kwargs:
params: Query parametersjson: JSON data for request bodyheaders: Custom headerstimeout: Request timeout in secondsfollow_redirects: Whether to follow redirects
TypedResponse[T]
A generic wrapper containing validated response data.
Attributes
| Attribute | Type | Description |
|---|---|---|
response |
httpx.Response |
Original HTTP response |
data |
T |
Validated and typed response data |
Global Instance
A pre-configured global instance is available:
from typed_requests import networking_manager
# Ready to use without initialization
response = await networking_manager.get(url)
🛡️ Best Practices
1. Define Response Types Upfront
# types/api.py
from typing import TypedDict, List, Optional
class PaginatedResponse(TypedDict):
items: List[dict]
total: int
page: int
per_page: int
has_next: bool
class ErrorResponse(TypedDict):
error: str
message: str
code: Optional[str]
2. Create API Client Classes
from typed_requests import NetworkingManager
from typing import Optional
class MyAPIClient:
def __init__(self, api_key: str):
self.manager = NetworkingManager()
self.api_key = api_key
self.base_url = "https://api.example.com"
async def __aenter__(self):
await self.manager.startup()
return self
async def __aexit__(self, *args):
await self.manager.shutdown()
def _headers(self) -> dict:
return {"X-API-Key": self.api_key}
async def get_user(self, user_id: int) -> UserResponse:
response = await self.manager.get(
f"{self.base_url}/users/{user_id}",
headers=self._headers(),
expected_type=UserResponse
)
return response.data
# Usage
async with MyAPIClient("your-api-key") as client:
user = await client.get_user(123)
print(user['name'])
3. Handle Errors Gracefully
from type_enforcer import ValidationError
import httpx
async def safe_api_call(url: str, expected_type: type) -> Optional[dict]:
try:
response = await networking_manager.get(url, expected_type=expected_type)
return response.data
except ValidationError as e:
logger.error(f"Response validation failed: {e}")
# Log the actual response for debugging
raw_response = await networking_manager.get(url)
logger.debug(f"Raw response: {raw_response.text}")
return None
except httpx.HTTPStatusError as e:
logger.error(f"HTTP {e.response.status_code}: {e.response.text}")
return None
except httpx.RequestError as e:
logger.error(f"Request failed: {e}")
return None
🔧 Development
Setup
# Clone the repository
git clone https://github.com/divinescreener/typed-requests
cd typed-requests
# Install dependencies
uv sync
# Run tests
uv run pytest
# Run linting
uv run ruff check
uv run mypy src
Contributing
We welcome contributions! Please see our Contributing Guidelines for details.
Key points:
- Maintain 100% test coverage
- Follow the existing code style
- Add tests for new features
- Update documentation as needed
📊 Performance
Typed Requests is built for production use:
- Connection Pooling: Reuses connections for better performance
- HTTP/2 Support: Multiplexed requests when supported
- Async Throughout: Non-blocking I/O for high concurrency
- Smart Validation: Only validates when
expected_typeis provided
🔒 Security
- TLS Verification: Secure by default with certificate validation
- No Code Execution: Type validation never executes arbitrary code
- Timeout Protection: Prevents hanging on slow endpoints
- Memory Efficient: Streams large responses when needed
📄 License
MIT License - see LICENSE for details.
🙏 Acknowledgments
- Built on the excellent httpx library
- Type validation powered by divine-type-enforcer
- Inspired by the need for safer API integrations
Made with ❤️ by DIVINE
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 divine_typed_requests-0.1.32.tar.gz.
File metadata
- Download URL: divine_typed_requests-0.1.32.tar.gz
- Upload date:
- Size: 57.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c088306584ce41d290f4dcf393dd2e228f5d9bd5f25c8a97a232f4d517bb271c
|
|
| MD5 |
3d2e699b213c74f8cd9e554d06410637
|
|
| BLAKE2b-256 |
f90a207d3186b54eee8b4b34ad13fe0d74c08e1b2075daa9d6348ed790cf3ff0
|
Provenance
The following attestation bundles were made for divine_typed_requests-0.1.32.tar.gz:
Publisher:
pypi-publish.yml on divinescreener/typed-requests
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
divine_typed_requests-0.1.32.tar.gz -
Subject digest:
c088306584ce41d290f4dcf393dd2e228f5d9bd5f25c8a97a232f4d517bb271c - Sigstore transparency entry: 314977929
- Sigstore integration time:
-
Permalink:
divinescreener/typed-requests@76b5abaaa48e7362b61757e7601654994e69e041 -
Branch / Tag:
refs/tags/v0.1.32 - Owner: https://github.com/divinescreener
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@76b5abaaa48e7362b61757e7601654994e69e041 -
Trigger Event:
push
-
Statement type:
File details
Details for the file divine_typed_requests-0.1.32-py3-none-any.whl.
File metadata
- Download URL: divine_typed_requests-0.1.32-py3-none-any.whl
- Upload date:
- Size: 10.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2946ab324752572083380786ad1b40a397af59a6e6c3429570e84bbc73869c29
|
|
| MD5 |
896c81cfea66e37b84d558f89bc87708
|
|
| BLAKE2b-256 |
69c9f1d0ff34780a881476189d483d5e4c395067b8a4ab1e31d7c2c6d9908e40
|
Provenance
The following attestation bundles were made for divine_typed_requests-0.1.32-py3-none-any.whl:
Publisher:
pypi-publish.yml on divinescreener/typed-requests
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
divine_typed_requests-0.1.32-py3-none-any.whl -
Subject digest:
2946ab324752572083380786ad1b40a397af59a6e6c3429570e84bbc73869c29 - Sigstore transparency entry: 314977941
- Sigstore integration time:
-
Permalink:
divinescreener/typed-requests@76b5abaaa48e7362b61757e7601654994e69e041 -
Branch / Tag:
refs/tags/v0.1.32 - Owner: https://github.com/divinescreener
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@76b5abaaa48e7362b61757e7601654994e69e041 -
Trigger Event:
push
-
Statement type: