ADR-005: Provide Both Sync and Async APIs¶
Status¶
Accepted
Date: 2024-07-01 (generated client includes both)
Context¶
Modern Python applications use both synchronous and asynchronous patterns:
- Async: Web servers (FastAPI, Sanic), high-concurrency apps
- Sync: Scripts, Jupyter notebooks, simple applications, legacy code
The client generator (openapi-python-client) can generate:
- Async-only (asyncio)
- Sync-only
- Both sync and async
Considerations:
- Not all users want to use async/await
- Some environments don't support async well (REPL, notebooks)
- Async provides better performance for concurrent operations
- Supporting both means larger codebase
Decision¶
We will provide both synchronous and asynchronous APIs for all endpoints.
Every generated endpoint module includes:
sync_detailed()- Synchronous, returns full Responsesync()- Synchronous, returns parsed data onlyasyncio_detailed()- Async, returns full Responseasyncio()- Async, returns parsed data only
Example from api/product/get_all_products.py:
# Synchronous
def sync_detailed(...) -> Response[ProductListResponse]:
"""List all products (sync)."""
...
def sync(...) -> ProductListResponse | None:
"""List all products (sync, parsed only)."""
...
# Asynchronous
async def asyncio_detailed(...) -> Response[ProductListResponse]:
"""List all products (async)."""
...
async def asyncio(...) -> ProductListResponse | None:
"""List all products (async, parsed only)."""
...
Users choose based on their needs:
# Async application (recommended for web servers)
async def main():
async with KatanaClient() as client:
response = await get_all_products.asyncio_detailed(client=client)
products = response.parsed.data
# Sync application (scripts, notebooks)
def main():
with KatanaClient() as client:
response = get_all_products.sync_detailed(client=client)
products = response.parsed.data
Consequences¶
Positive Consequences¶
- Universal Compatibility: Works in any Python environment
- User Choice: Users pick what fits their architecture
- No Migration Required: Can start sync, move to async later
- REPL/Notebook Friendly: Sync works in interactive environments
- Script Friendly: No async complexity for simple scripts
- Performance When Needed: Async available for concurrent operations
- Complete: Both APIs have same functionality
Negative Consequences¶
- Larger Codebase: 2× the endpoint methods (~500 methods total)
- Maintenance: Need to ensure both work correctly
- Documentation: Must document both patterns
- Choice Paralysis: New users may not know which to use
Neutral Consequences¶
- Generated Code: Generator handles both automatically
- Testing: Need tests for both sync and async paths
Alternatives Considered¶
Alternative 1: Async Only¶
Provide only async API:
async def main():
async with KatanaClient() as client:
response = await get_all_products.asyncio_detailed(client=client)
Pros:
- Smaller codebase
- Forces modern async patterns
- Better performance potential
- Simpler to maintain
Cons:
- Doesn't work in notebooks/REPL
- Complex for simple scripts
- Requires async knowledge
- Excludes sync-only users
Why Rejected: Too limiting, excludes valid use cases.
Alternative 2: Sync Only¶
Provide only sync API:
Pros:
- Simpler for beginners
- Works everywhere
- No async complexity
- Smaller codebase
Cons:
- Can't leverage async performance
- Doesn't fit modern async applications
- Poor performance for concurrent operations
- Not future-proof
Why Rejected: Limits performance, not modern.
Alternative 3: Sync with Async Wrapper¶
Provide sync API, users can wrap in async:
import asyncio
async def async_wrapper():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
lambda: get_all_products.sync_detailed(client=client)
)
Pros:
- One codebase (sync)
- Can "fake" async
Cons:
- Not true async (no concurrency benefit)
- Complex boilerplate for users
- Poor performance
- Misleading API
Why Rejected: Fake async is worse than true async or sync.
Usage Guidance¶
When to Use Async¶
✅ Use async when:
- Building web servers (FastAPI, Sanic, etc.)
- Need concurrent operations (multiple API calls in parallel)
- Application is already async
- Performance is critical
Example:
# Concurrent requests (much faster than sync)
async with KatanaClient() as client:
products_task = get_all_products.asyncio_detailed(client=client)
orders_task = get_all_sales_orders.asyncio_detailed(client=client)
products, orders = await asyncio.gather(products_task, orders_task)
When to Use Sync¶
✅ Use sync when:
- Writing scripts or CLI tools
- Working in Jupyter notebooks
- Learning the API
- Application is synchronous
- Simplicity is more important than performance
Example:
# Simple script
with KatanaClient() as client:
response = get_all_products.sync_detailed(client=client)
for product in response.parsed.data:
print(product.name)
Implementation Details¶
Both APIs Get Same Features¶
Resilience features work for both:
- ✅ Automatic retries
- ✅ Rate limit handling
- ✅ Auto-pagination
- ✅ Error handling
Transport Layer Handles Both¶
KatanaClient provides both sync and async transports:
class KatanaClient(AuthenticatedClient):
def __init__(self, **kwargs):
# Async transport (default)
async_transport = ResilientAsyncTransport.create(...)
# Sync transport (also available)
sync_transport = ResilientSyncTransport.create(...)
super().__init__(
transport=sync_transport,
async_transport=async_transport,
**kwargs
)
Examples Provided¶
Both patterns documented:
examples/basic_usage.py- Async examplesexamples/sync_usage.py- Sync examples