Dead-simple interface for sync/async unification
Project description
syncflex
A tiny Python library that lets you write a single generator function and run it seamlessly in sync or async contexts.
It provides two decorators:
-
syncify→ turns a generator into a blocking synchronous function. -
asyncify→ turns a generator into an awaitable coroutine function.
This allows you to share the same core generator logic in both sync and async code paths without duplication.
🚀 Installation
pip install syncflex
(or copy the ~30 lines of code straight into your project.)
✨ Motivation
Library authors often need to support both synchronous and asynchronous API.
Traditionally, that means a lot of duplicated code:
from typing import Any
import httpx
def pre_processing_code() -> Any: ...
def post_processing_code() -> Any: ...
def sync_api_call(http_client: httpx.Client) -> str:
pre_processing_code()
pre_processing_code()
pre_processing_code()
response = http_client.request(...)
post_processing_code()
post_processing_code()
post_processing_code()
return "hello"
async def async_api_call(http_client: httpx.AsyncClientClient) -> str:
pre_processing_code()
pre_processing_code()
pre_processing_code()
response = await http_client.request(...)
post_processing_code()
post_processing_code()
post_processing_code()
return "hello"
With syncflex, you can unify the implementation code and expose both sync and async API like so:
from collections.abc import Generator
from typing import Any
import httpx
from syncflex import asyncify, syncify
def pre_processing_code() -> Any: ...
def post_processing_code() -> Any: ...
def _base_api_call(http_client: httpx.Client | httpx.AsyncClient) -> Generator[Any, Any, str]:
pre_processing_code()
pre_processing_code()
pre_processing_code()
response: httpx.Response = yield http_client.request(...)
post_processing_code()
post_processing_code()
post_processing_code()
return "hello"
sync_api_call = syncify(_base_api_call)
async_api_call = asyncify(_base_api_call)
📖 Usage
Simple SDK
Below is an quick SDK implementation of JSONPlaceholder.
Without syncflex:
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Self
import httpx
@dataclass
class Post:
id: int
user_id: int
title: str
body: str
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Self:
return cls(id=data["id"], user_id=data["userId"], title=data["title"], body=data["body"])
class SyncSDK:
def __init__(self, http_client: httpx.Client) -> None:
self._http_client = http_client
def get_post(self, id: int) -> Post:
url = f"/posts/{id}"
resp: httpx.Response = self._http_client.get(url)
return Post.from_dict(resp.json())
def list_posts(self) -> list[Post]:
url = "/posts"
resp: httpx.Response = self._http_client.get(url)
return [Post.from_dict(data) for data in resp.json()]
class AsyncSDK:
def __init__(self, http_client: httpx.AsyncClient) -> None:
self._http_client = http_client
async def get_post(self, id: int) -> Post:
url = f"/posts/{id}"
resp: httpx.Response = await self._http_client.get(url)
return Post.from_dict(resp.json())
async def list_posts(self) -> list[Post]:
url = "/posts"
resp: httpx.Response = await self._http_client.get(url)
return [Post.from_dict(data) for data in resp.json()]
With syncflex:
from collections.abc import Generator, Mapping
from dataclasses import dataclass
from typing import Any, Self
import httpx
from syncflex import asyncify, syncify
@dataclass
class Post:
id: int
user_id: int
title: str
body: str
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Self:
return cls(id=data["id"], user_id=data["userId"], title=data["title"], body=data["body"])
class BaseSDK:
def __init__(self, http_client: httpx.Client | httpx.AsyncClient) -> None:
self._http_client = http_client
def _get_post(self, id: int) -> Generator[Any, Any, Post]:
url = f"/posts/{id}"
# Yield on the part where it could be sync/async
resp: httpx.Response = yield self._http_client.get(url)
return Post.from_dict(resp.json())
def _list_posts(self) -> Generator[Any, Any, list[Post]]:
url = "/posts"
# Yield on the part where it could be sync/async
resp: httpx.Response = yield self._http_client.get(url)
return [Post.from_dict(data) for data in resp.json()]
class SyncSDK(BaseSDK):
get_post = syncify(BaseSDK._get_post)
list_posts = syncify(BaseSDK._list_posts)
class AsyncSDK(BaseSDK):
get_post = asyncify(BaseSDK._get_post)
list_posts = asyncify(BaseSDK._list_posts)
See more examples in examples/
⚖️ License
MIT
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 syncflex-0.1.0.tar.gz.
File metadata
- Download URL: syncflex-0.1.0.tar.gz
- Upload date:
- Size: 3.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5786f4e9da228d1bbd4ebb5652e50215bcdfbda4735ee59eb0302f06478d5330
|
|
| MD5 |
ae1ae5519511148a97afe6e7c7de5d96
|
|
| BLAKE2b-256 |
ae5c6daeba6d0724f666fb89060336893c1d637c8fd5c37036606e90b87ceab6
|
File details
Details for the file syncflex-0.1.0-py3-none-any.whl.
File metadata
- Download URL: syncflex-0.1.0-py3-none-any.whl
- Upload date:
- Size: 3.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5010022cae1586c5aab64715a7068330d2af5b0fdf4d2aca866ec67c2cf47908
|
|
| MD5 |
79545235398333b6a6e51248c37e94fb
|
|
| BLAKE2b-256 |
97f969f7c4d949f1c0e977630106a223f97ae510352208583af2c1ef0fc4fde1
|