Skip to main content

Generator-based async framework

This project was created for educational purpose to demonstrate people how to write their own async framework with event loop, socket primitives and an HTTP server.

Lib

The lib is pretty similar with asyncio module. Here's a simple example of an async program:

from typing import Generator
import gensyncio


def main() -> Generator[None, None, None]:
    yield from gensyncio.sleep(1)
    print("Hello from async!")


print(gensyncio.run(main()))

You can achieve real async by creating tasks in an event loop and gathering them.

from typing import Generator
import gensyncio


def print_after_delay(msg: str, delay: int):
    yield from gensyncio.sleep(delay)
    print(msg)


def main() -> Generator[None, None, None]:
    yield from gensyncio.gather(
        print_after_delay("Hello", 1),
        print_after_delay("from", 1),
        print_after_delay("async", 1),
    )


gensyncio.run(main())

Sync primitives

This lib implements syncronization primitives for async programming. Such as:

  • Event
  • Lock

Here are examples of how they should be used:

An event:

import gensyncio


def waiter(event: gensyncio.Event):
    print("waiting for it ...")
    yield from event.wait()
    print("... got it!")


def main():
    # Create an Event object.
    event = gensyncio.Event()

    # Spawn a Task to wait until 'event' is set.
    waiter_task = gensyncio.create_task(waiter(event))

    # Sleep for 1 second and set the event.
    yield from gensyncio.sleep(1)
    event.set()

    # Wait until the waiter task is finished.
    yield from waiter_task


gensyncio.run(main())

A lock:

from typing import Generator
import gensyncio


def print_after(lock: gensyncio.Lock, delay: float, val: str) -> Generator[None, None, None]:
    """Print after delay, but wit aquiring a lock."""
    # Here we are using the lock as a context manager
    with lock as _lock:
        # This will yield from the lock, and wait until the lock is released
        yield from _lock
        # This will yield from the sleep, and wait until the sleep is done
        yield from gensyncio.sleep(delay)
    print(val)


def main() -> Generator[None, None, None]:
    loop = gensyncio.get_running_loop()
    lock = gensyncio.Lock()
    loop.create_task(print_after(lock, 2, "one"))
    t = loop.create_task(print_after(lock, 1, "two"))
    # Here we wait for the task to finish
    yield from t


gensyncio.run(main())

Queue

The queue is the same as asyncio Queue. This example is rewritten asyncio.Queue example from python docs.

import random
import time

import gensyncio


def worker(name: str, queue: gensyncio.Queue[float]):
    while True:
        # Get a "work item" out of the queue.
        sleep_for = yield from queue.get()

        # Sleep for the "sleep_for" seconds.
        yield from gensyncio.sleep(sleep_for)

        # Notify the queue that the "work item" has been processed.
        queue.task_done()

        print(f"{name} has slept for {sleep_for:.2f} seconds")


def main():
    # Create a queue that we will use to store our "workload".
    queue = gensyncio.Queue()

    # Generate random timings and put them into the queue.
    total_sleep_time = 0
    for _ in range(20):
        sleep_for = random.uniform(0.05, 1.0)
        total_sleep_time += sleep_for
        queue.put_nowait(sleep_for)

    # Create three worker tasks to process the queue concurrently.
    tasks = []
    for i in range(3):
        task = gensyncio.create_task(worker(f"worker-{i}", queue))
        tasks.append(task)

    # Wait until the queue is fully processed.
    started_at = time.monotonic()
    yield from queue.join()
    total_slept_for = time.monotonic() - started_at

    # Cancel our worker tasks.
    for task in tasks:
        try:
            task.cancel()
        except gensyncio.GenCancelledError:
            pass
    # Wait until all worker tasks are cancelled.
    yield from gensyncio.gather(*tasks)

    print("====")
    print(f"3 workers slept in parallel for {total_slept_for:.2f} seconds")
    print(f"total expected sleep time: {total_sleep_time:.2f} seconds")


gensyncio.run(main())

Sockets

Also this lib contains a simple socket implementation which is compatible with generators approach. Here's a simple example of using generator based socket:

import socket
from typing import Generator
import gensyncio
from gensyncio.gensocket import GenSocket


def main() -> Generator[None, None, None]:
    sock = GenSocket(socket.AF_INET, socket.SOCK_STREAM)
    yield from sock.connect(("httpbin.org", 80))
    sock.send(b"GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n")
    resp = yield from sock.recv(1024)
    print(resp.decode("utf-8"))


gensyncio.run(main())

GenSocket is alsmost similar to socket.socket except it's always nonblocking and some of it's methods should be awaited using yield from.

Http module

Also there's a small HTTP module which you can use to serve and send requests.

Here's a client usage example:

import gensyncio
from gensyncio.http import ClientRequest


def main():
    yield
    req = ClientRequest("http://localhost:8080/", "GET", json={"one": "two"})
    resp = yield from req.send()
    print(resp)
    print(resp.body.decode("utf-8"))


gensyncio.run(main())

And here's a simple echo server example:

import logging
from typing import Generator
from gensyncio.http import Server
import gensyncio
from gensyncio.http.server import Request, Response

app = Server()


@app.router.get("/")
def index(req: Request) -> Generator[None, None, Response]:
    body = yield from req.read()
    return Response(status=200, body=body, content_type=req.content_type)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    gensyncio.run(app.run(host="0.0.0.0", port=8080))

Metadata

Release files for gensyncio 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gensyncio 1.0.0
File Size Uploaded
gensyncio-1.0.0.tar.gz 12.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gensyncio 1.0.0
File Interpreter ABI Platform
gensyncio-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.3 kB

Release files / gensyncio-1.0.0.tar.gz

Download URL gensyncio-1.0.0.tar.gz
Size 12.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5e3dfe2f01990b6560d54650b42ac0285e806edf168072297a0ce5fa8a529efc
BLAKE2b-256 checksum
How to use checksums
2120d2b4d350e9632116b6e36a3e6d91bb06af5d22be0e09fd33bc2d691e9ab4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.4 CPython/3.10.12 Linux/6.5.0-1025-azure

Release files / gensyncio-1.0.0-py3-none-any.whl

Download URL gensyncio-1.0.0-py3-none-any.whl
Size 14.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7f9f0e955a2953d650f40318d6ad0745326c7569b068eaa3fd912c1095999e3f
BLAKE2b-256 checksum
How to use checksums
1227138a54859a11971f4beaec1d6083d53a71bc7113f52683a84669d8d8de9a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.4 CPython/3.10.12 Linux/6.5.0-1025-azure

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page