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 conjurer
- a simple in-process conduit for background spells
- the
Runicruntime facade for typed queries, commands, events, and background work - generic request primitives
- generic
Ok/Errresult 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: conjurer
from dataclasses import dataclass
from runic import DefaultError, Ok, Result, create_conjurer
@dataclass(slots=True)
class Ping:
value: str
class PingService:
async def emit(self, data: Ping) -> Result[str, DefaultError]:
return Ok(f"pong:{data.value}")
conjurer = create_conjurer()
handler, key = conjurer.conjure(PingService())
same_handler = conjurer.retrieve(key)
result = await same_handler.emit(Ping(value="hello"))
Example: conduit
from runic import Conduit, Ok, create_bus
bus = create_bus(object)
conduit = Conduit(bus)
status_events = conduit.status_events()
log_events = conduit.log_events()
async def work(ctx):
await ctx.log("starting")
await ctx.progress(1.0)
return Ok({"done": True})
spell_id = await conduit.invoke(work)
record = conduit.get_status(spell_id)
status = await anext(status_events)
log = await anext(log_events)
get_status(...) returns Ok(SpellRecord(...)) for known spells and Err(DefaultError(...)) for unknown spell ids.
You can also pass a spellbook to share state across spells:
from runic import Conduit, InMemorySpellBook, create_bus
spellbook = InMemorySpellBook()
conduit = Conduit(create_bus(dict), spellbook=spellbook)
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.conjure(UserService())
balance_handler = runic.conjure(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:
conjure(name, ...)call(name, payload)query(...)spell("name")invoke("name", payload)emit("topic", payload)
Public API
create_bus(shape)creates an in-memory event bus with runtime payload checksConjurerregisters concrete services and retrieves typed handlers by keyConduitruns background spells and publishes typed status/log streamsHandler[TService]wraps object services and exposes typedask(...)andinvoke(...)Runicexposes typedask(...), broad-querypublish(...), eventemit(...), andinvoke(...)helpers plusconjure(...),query(...),spell(...), andon(...)OkandErrprovide lightweight result containers
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 runic_io-0.1.3.tar.gz.
File metadata
- Download URL: runic_io-0.1.3.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e83d8c985abaea0501f2d429778e00089bc34d0ad05a128d5cad7e957b65f8da
|
|
| MD5 |
9734fae5ed3ed0f4f25395ce66af7e03
|
|
| BLAKE2b-256 |
905ac5f30bfc03e826fa9910b49e1309c36dc9dd43fa939bb4fe8f0b7f9c1dd0
|
File details
Details for the file runic_io-0.1.3-py3-none-any.whl.
File metadata
- Download URL: runic_io-0.1.3-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51b0a7b4b89e027e32c29e2e4cedaf1b70c7a2c3affe51e60d88ebc87b40190f
|
|
| MD5 |
5231a96b2b679adcbb0a0ee916cb05bd
|
|
| BLAKE2b-256 |
79d1f2ff671b24fbd9e7f2e4361feafa34d42e298748e1209a9dcbb51d4ae1ec
|