Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

syncflex-0.1.0.tar.gz (3.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

syncflex-0.1.0-py3-none-any.whl (3.9 kB view details)

Uploaded Python 3

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

Hashes for syncflex-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5786f4e9da228d1bbd4ebb5652e50215bcdfbda4735ee59eb0302f06478d5330
MD5 ae1ae5519511148a97afe6e7c7de5d96
BLAKE2b-256 ae5c6daeba6d0724f666fb89060336893c1d637c8fd5c37036606e90b87ceab6

See more details on using hashes here.

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

Hashes for syncflex-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5010022cae1586c5aab64715a7068330d2af5b0fdf4d2aca866ec67c2cf47908
MD5 79545235398333b6a6e51248c37e94fb
BLAKE2b-256 97f969f7c4d949f1c0e977630106a223f97ae510352208583af2c1ef0fc4fde1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page