A “write-once, run‐anywhere” sync/async bridge that’s thread-safe, decorator-driven, and plays nicely in FastAPI (or other frameworks) & with DB connections.
Project description
synchronaut Overview
synchronaut is a tiny bridge to write your business logic once and run it in both sync and async contexts—thread-safe, decorator-driven, and DB-friendly. It provides:
- A single
smart_callentrypoint for all sync↔️async combinations - A decorator
@synchronaut(...)with.sync/.async_bypass methods - Batch helper
smart_map - Context-var propagation across threads
- Customizable timeouts with
SmartCallTimeout
Quickstart
Install:
pip install synchronaut
Create quickstart.py:
import time
import asyncio
from synchronaut import synchronaut, smart_call, smart_map, SmartCallTimeout
# ——— plain functions ———
def sync_add(a, b):
return a + b
async def async_add(a, b):
return a + b
# ——— decorated versions ———
@synchronaut()
def dec_sync_add(a, b):
return a + b
@synchronaut(timeout=1.0)
async def dec_async_add(a, b):
return a + b
async def main():
# sync → sync
print('sync_add:', sync_add(1, 2))
print('smart_call(sync_add):', await smart_call(sync_add, 3, 4))
# sync → async (in async context, sync funcs auto-offload)
print('offloaded sync_add:', await smart_call(sync_add, 5, 6))
# async → async
print('async_add:', await async_add(7, 8))
print('smart_call(async_add):', await smart_call(async_add, 7, 8))
# batch helper in async
print('smart_map:', await smart_map([sync_add, async_add], 4, 5))
# decorator shortcuts in async
print('await dec_sync_add.async_:', await dec_sync_add.async_(6, 7))
print('await dec_async_add:', await dec_async_add(8, 9))
# timeout demo (pure-sync offload)
try:
await smart_call(lambda: time.sleep(2), timeout=0.5)
except SmartCallTimeout as e:
print('Timeout caught:', e)
if __name__ == '__main__':
# sync-land examples
print('dec_sync_add(2,3):', dec_sync_add(2, 3))
print('smart_call(async_add) in sync:', smart_call(async_add, 9, 10))
# then run the async demonstrations
asyncio.run(main())
Run it:
python quickstart.py
Expected output:
dec_sync_add(2,3): 5
sync_add: 3
smart_call(sync_add): 7
offloaded sync_add: 11
async_add: 15
smart_call(async_add): 15
smart_map: [9, 9]
await dec_sync_add.async_: 13
await dec_async_add: 17
Timeout caught: Function <lambda> timed out after 0.5s
FastAPI Integration
Copy this into app.py—it’ll just work once you pip install synchronaut:
from typing import AsyncGenerator
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from synchronaut import synchronaut
# ——— Dummy DB & models ———
class User(BaseModel):
id: int
name: str
class DummyDB:
def __init__(self):
self._data = {
1: {'id': 1, 'name': 'Alice'},
2: {'id': 2, 'name': 'Bob'},
}
def query(self, user_id: int):
return self._data.get(user_id)
async def get_db_async() -> AsyncGenerator[DummyDB, None]:
db = DummyDB()
try:
yield db
finally:
...
# ——— App & routes ———
app = FastAPI()
@synchronaut()
def get_user(user_id: int, db: DummyDB = Depends(get_db_async)) -> User:
data = db.query(user_id)
if not data:
raise HTTPException(status_code=404, detail='User not found')
return User(**data)
@app.get('/')
async def hello():
@app.get('/users/{user_id}', response_model=User)
async def read_user(user: User = Depends(get_user)):
return user
Run:
uvicorn app:app --reload
This will produce:
When you go to http://127.0.0.1:8000/ -> {'Hello, @syncronauts!'}
When you go to http://127.0.0.1:8000/users/1 -> {'id': 1, 'name': 'Alice'}
When you go to http://127.0.0.1:8000/users/2 -> {'id': 2, 'name': 'Bob'}
When you go to http://127.0.0.1:8000/users/3 -> {"detail":"User not found"}
Context Propagation
Put this in ctx_prop.py:
from synchronaut.utils import (
request_context,
spawn_thread_with_ctx,
set_request_ctx,
get_request_ctx,
)
# set a global context
set_request_ctx({'user_id': 42})
print('Global, user_id:', get_request_ctx()['user_id']) # 42
# override in a block
with request_context({'user_id': 99}):
print('Inside block, user_id:', get_request_ctx()['user_id']) # 99
# back to global
print('Global again, user_id:', get_request_ctx()['user_id']) # 42
# worker in a thread sees the global context
def work():
print('Inside thread, user_id:', get_request_ctx()['user_id']) # 42
thread = spawn_thread_with_ctx(work)
thread.join()
Run:
python ctx_prop.py
Expected:
Global, user_id: 42
Inside block, user_id: 99
Global again, user_id: 42
Inside thread, user_id: 42
Advanced
All these options are callable via smart_call(...) or the @synchronaut(...) decorator:
timeout=: raisesSmartCallTimeoutif the call exceeds N secondsforce_offload=True: always run sync funcs in the background loop (enables timely cancellation)reuse_loop=True: submit async coroutines to a long-lived background loopsmart_map([...], *args): runs in parallel in async context, sequentially in sync context- Context propagation:
set_request_ctx()/get_request_ctx()to set and read a globalContextVarrequest_context({...})context-manager to temporarily overridespawn_thread_with_ctx(fn, *args)to ensureContextVarstate flows into threads
⚠️ Gotchas
- Decorator overhead: each call does an inspect/async-check (nanoseconds–µs). In ultra-hot loops, consider a bypass.
- Timeouts on sync code: pure-sync calls only respect
timeoutif offloaded—otherwise they block until completion. - Background loop lifecycle: offloads and
.syncbypass use our single background loop; it lives until process exit. - ContextVar propagation: manual threads must use our
spawn_thread_with_ctx. - Non-asyncio stacks:
_in_async_contextrecognizes only asyncio and Trio. Other event loops may mis-route. - Tracebacks: decorators + offloads can obscure original frames. Use logging or
inspect.trace()for debugging.
✅ When to use synchronaut
- I/O-bound web services (DB calls, HTTP, file I/O)
- Mixed sync/async code-bases (one API, two contexts)
- FastAPI / DI: sync ORMs auto-offload under the hood
- Context-scoped resources: single “request context” across threads & coros
🚫 When not to use synchronaut
- CPU-bound tight loops where microseconds matter
- Pure-sync or pure-async projects (no context switching)
- Non-asyncio async frameworks (e.g. Curio)
- Very high-volume coroutine batches in sync code without
reuse_loop - Strict loop-lifecycle environments that forbid background loops
By tuning
timeout,force_offload,reuse_loop, or using the.sync/.async_bypasses, you get seamless sync↔️async interoperability without rewriting your core logic.
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 synchronaut-0.1.0.tar.gz.
File metadata
- Download URL: synchronaut-0.1.0.tar.gz
- Upload date:
- Size: 28.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c3c0481e43964e191579329eaff6fe778e7809ff9fadad7afb19d077ef34d6a
|
|
| MD5 |
f777014886aa62b6fd3a8e4d5712f0ed
|
|
| BLAKE2b-256 |
505b0d2dd49eacec5e636b6192956f1d8c498fbc928a50dc43686d6b3afab0eb
|
File details
Details for the file synchronaut-0.1.0-py3-none-any.whl.
File metadata
- Download URL: synchronaut-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.6.17
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a35f03a1f6e93221667f2dd360524d109ed5c7818dac3cc31c4e987f6b3a123
|
|
| MD5 |
341b79b74ac1ff851eadcb8021d664f9
|
|
| BLAKE2b-256 |
3da67279e210a011ad9487e079824cdd72d627b4373387c15daaf09d276e2aa3
|