Skip to main content

Small async utility library for event-driven application code.

Project description

Runic

Runic is a small async utility library for event-driven application code.

The package is published on PyPI as runic-io and imported in Python as runic.

It provides:

  • a typed in-memory event bus
  • a typed service dispatcher
  • a simple in-process job runtime
  • the Runic runtime facade for typed queries, commands, events, and background work
  • generic request primitives
  • generic Ok / Err result types

Runic targets small, composable building blocks rather than a large framework.

Requirements

  • Python 3.12+

Development Setup

uv venv .venv
source .venv/bin/activate
uv pip install -e .[dev]

Installation

uv add runic-io
uv pip install runic-io
pip install runic-io
import runic
from runic import Runic

Run tests with:

python -m unittest discover -s tests -v

Example: event bus

from runic import Event, create_bus

bus = create_bus(dict)
subscriber = bus.subscribe()
await bus.publish(Event(name="ready", data={"ok": True}))
event = await anext(subscriber)

Example: dispatcher

from dataclasses import dataclass

from runic import DefaultError, Ok, Result, create_dispatcher


@dataclass(slots=True)
class Ping:
    value: str


class PingService:
    async def emit(self, data: Ping) -> Result[str, DefaultError]:
        return Ok(f"pong:{data.value}")


dispatcher = create_dispatcher()
handler, key = dispatcher.register(PingService())
same_handler = dispatcher.retrieve(key)
result = await same_handler.emit(Ping(value="hello"))

Example: jobs

from runic import JobManager, Ok, create_bus

bus = create_bus(object)
jobs = JobManager(bus)
status_events = jobs.status_events()
log_events = jobs.log_events()


async def work(ctx):
    await ctx.log("starting")
    await ctx.progress(1.0)
    return Ok({"done": True})


job_id = await jobs.start(work)
record = jobs.get_status(job_id)
status = await anext(status_events)
log = await anext(log_events)

get_status(...) returns Ok(JobRecord(...)) for known jobs and Err(DefaultError(...)) for unknown job ids.

You can also pass a task backend to share state across jobs:

from runic import InMemoryTaskBackend, JobManager, create_bus

backend = InMemoryTaskBackend()
jobs = JobManager(create_bus(dict), backend=backend)


async def work(ctx):
    ctx.shared["runs"] = int(ctx.shared.get("runs", 0)) + 1
    return {"runs": ctx.shared["runs"]}

Example: object handler runtime

from dataclasses import dataclass
from decimal import Decimal

from runic import Command, DefaultError, Ok, Query, Runic


@dataclass(slots=True)
class GetUser(Query[dict[str, int], DefaultError]):
    user_id: int


@dataclass(slots=True)
class RenameUser(Command[str, DefaultError]):
    user_id: int
    name: str


@dataclass(slots=True)
class GetBalance(Query[dict[str, Decimal], DefaultError]):
    user_id: int


@dataclass(slots=True)
class UserRequested:
    user_id: int


class UserService:
    async def ask(self, query: GetUser) -> Ok[dict[str, int]]:
        return Ok({"user_id": query.user_id})

    async def invoke(self, command: RenameUser) -> Ok[str]:
        return Ok(f"renamed:{command.user_id}:{command.name}")


class BalanceService:
    async def ask(self, query: GetBalance) -> Ok[dict[str, Decimal]]:
        return Ok({"balance": Decimal("10.50")})


runic = Runic()
user_handler = runic.register(UserService())
balance_handler = runic.register(BalanceService())


@runic.on(UserRequested)
async def on_user_requested(event: UserRequested) -> None:
    print("user requested", event.user_id)


await runic.emit(UserRequested(user_id=1))
user_result = await user_handler.ask(GetUser(user_id=1))
rename_result = await user_handler.invoke(RenameUser(user_id=1, name="Ada"))
all_balances = await runic.publish(GetBalance(user_id=1))
direct_balance = await balance_handler.ask(GetBalance(user_id=1))

The runtime also still supports the older APIs:

  • register(name, ...)
  • call(name, payload)
  • query(...)
  • task("name")
  • dispatch(name, payload)
  • emit("topic", payload)

Public API

  • create_bus(shape) creates an in-memory event bus with runtime payload checks
  • Dispatcher registers concrete services and retrieves typed handlers by key
  • JobManager runs background jobs and publishes typed status/log streams
  • Handler[TService] wraps object services and exposes typed ask(...) and invoke(...)
  • Runic exposes typed ask(...), broad-query publish(...), event emit(...), and start(...) helpers plus register(...), query(...), task(...), and on(...)
  • Ok and Err provide lightweight result containers

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

runic_io-0.1.1.tar.gz (12.4 kB view details)

Uploaded Source

Built Distribution

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

runic_io-0.1.1-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file runic_io-0.1.1.tar.gz.

File metadata

  • Download URL: runic_io-0.1.1.tar.gz
  • Upload date:
  • Size: 12.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for runic_io-0.1.1.tar.gz
Algorithm Hash digest
SHA256 2bffcd49a3c05ebdd8c45a5d14a247e0b32a799c7690285db5a8e16b060635e1
MD5 3740219a22f14da75d48a50d769a034c
BLAKE2b-256 7fac9ab2d34b083c5b005b2a58d5406f56dee5bdd63e3bce29de6bb515459509

See more details on using hashes here.

File details

Details for the file runic_io-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: runic_io-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for runic_io-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d9eac5095e75f659b35c8541d7d44aebe8e360de7226d5576dc39cad02118ced
MD5 5f005ef7ced57b0fd8a39617502ad0b9
BLAKE2b-256 297dbcb51ff24f04debd54ca87f91ca8201c1648520e38d9a6c6daadd7f0a493

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