Metadata-Version: 2.4
Name: dust-riven
Version: 0.1.5
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 3 - Alpha
Requires-Dist: pytest>=7.0 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: A fast, thread-safe Signal (pub-sub) primitive for Python, written in Rust.
Keywords: signal,pubsub,rust,pyo3
Author-email: Minh1billion <minh01212929979@gmail.com>
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/Minh1billion/dust-riven
Project-URL: Issues, https://github.com/Minh1billion/dust-riven/issues
Project-URL: Repository, https://github.com/Minh1billion/dust-riven

# Dust Riven

![Rust](https://img.shields.io/badge/rust-stable-orange?logo=rust)
![PyO3](https://img.shields.io/badge/PyO3-extension-blue)
![Python](https://img.shields.io/badge/python-3.8%2B-blue?logo=python)
![License](https://img.shields.io/badge/license-MIT-green)

Dust Riven is a small Rust extension for Python, built with [PyO3](https://pyo3.rs/). It provides a `Signal` class, a simple event system similar to observer/pub-sub patterns. You create a `Signal`, connect callbacks to it, and `emit` it with arguments to call all connected callbacks.

## What it does

`Signal` is a thread-safe event emitter. It lets you register functions that run when the signal is emitted, and it supports:

- normal connections
- one-time connections
- connections that expire after a fixed number of calls
- weak references, so a callback can be garbage collected without needing to manually disconnect it

## Basic usage

```python
import dust_riven

signal = dust_riven.Signal("on_update")
```

- `connect(callback, weak=False)` - registers a callback, returns an id used to disconnect it later
- `connect_once(callback, weak=False)` - callback runs on the first emit only
- `connect_finite(callback, times, weak=False)` - callback runs for a fixed number of emits, then is removed
- `disconnect(id)` - removes a specific callback
- `emit(*args, **kwargs)` - calls all connected callbacks with the given arguments
- `emit_async(*args, **kwargs)` - like `emit`, but if a callback returns an awaitable (e.g. it's an `async def`), those are collected and run concurrently with `asyncio.gather`; must be awaited
- `len(signal)` - number of callbacks currently connected

Pass `weak=True` on any connect method to hold a weak reference instead of a strong one, so the callback can be garbage collected normally if nothing else references it.

## Examples

### Connect and emit

```python
import dust_riven

signal = dust_riven.Signal("on_update")

def handler(message):
    print("got:", message)

signal.connect(handler)
signal.emit("hello world")
```

### Connect once

```python
def setup_handler():
    print("setup ran")

signal.connect_once(setup_handler)
signal.emit()  # prints "setup ran"
signal.emit()  # does nothing, handler already removed
```

### Connect finite

```python
def limited_handler():
    print("called")

signal.connect_finite(limited_handler, 3)
signal.emit()  # called
signal.emit()  # called
signal.emit()  # called
signal.emit()  # nothing happens, removed after 3 calls
```

### Async emit

```python
import asyncio
import dust_riven

signal = dust_riven.Signal("on_update")

def sync_handler(value):
    print("sync:", value)

async def async_handler(value):
    await asyncio.sleep(0.1)
    print("async:", value)

signal.connect(sync_handler)
signal.connect(async_handler)

async def main():
    # sync_handler runs immediately; async_handler is awaited
    # concurrently alongside any other async listeners
    await signal.emit_async(42)

asyncio.run(main())
```

### Weak connection

```python
class Listener:
    def handle(self, value):
        print("received", value)

listener = Listener()
signal.connect(listener.handle, weak=True)

del listener
signal.emit(42)  # dead reference is dropped silently, no crash
```

### Disconnect

```python
cb_id = signal.connect(handler)
signal.disconnect(cb_id)
```

### Listener count

```python
print(len(signal))
```

## Behavior notes

- Callbacks are collected into a snapshot before being called, so connecting or disconnecting callbacks from inside another callback during `emit` is safe.
- If a weakly referenced callback has already been garbage collected, it is silently removed the next time `emit` runs.
- Callbacks connected with `connect_once` or `connect_finite` are automatically removed once they've been called the requested number of times.
- `emit_async` calls every callback the same way `emit` does; any callback that returns an awaitable (e.g. an `async def`) has that awaitable scheduled via `asyncio.gather` and run concurrently once you `await` the result. Purely synchronous callbacks run immediately, before the returned value is awaited.
- With `emit_async`, an exception from a synchronous callback is raised as soon as it's called (before you even reach the `await`), while an exception from an async callback surfaces when the gathered result is awaited.
- If a callback raises during `emit_async`, remaining callbacks are not called, and any async callback's coroutine already created before the error is closed rather than left dangling.

## Building

This project uses PyO3 and is built as a native Python extension, typically with [maturin](https://www.maturin.rs/). Once built, it exposes a Python module named `dust_riven` containing the `Signal` class.

### Requirements

- Rust toolchain
- Python with a PyO3-compatible version
- `parking_lot`, `once_cell`, and `smallvec` crates

## Running the benchmark against blinker

The repository includes `bench.py`, a script that compares `dust_riven` against the [blinker](https://pypi.org/project/blinker/) library for `connect` and `emit` performance, across different listener counts, emit counts, and strong vs weak callback variants.

**1. Create and activate a virtual environment**

```bash
python -m venv .venv
source .venv/bin/activate
```

On Windows, activate with `.venv\Scripts\activate` instead.

**2. Install the needed Python packages**

```bash
pip install blinker tqdm maturin
```

**3. Build and install dust_riven into the same venv**

```bash
maturin develop --release
```

Run this from the project root (the folder containing `Cargo.toml` and `pyproject.toml`). It compiles the Rust code and installs the `dust_riven` module directly into the active venv.

**4. Run the benchmark**

```bash
python bench.py
```

This runs the full matrix of listener counts and emit counts for both strong and weak variants, and prints comparison tables with minimum/median timings plus a speedup ratio.

For a fast smoke test instead of the full matrix:

```bash
python bench.py --quick
```

You can also narrow the run with options, for example:

```bash
python bench.py --listeners 1,100,1000 --emits 100,1000 --repeats 5 --variants strong --measure emit
```

Available options: `--listeners`, `--emits`, `--repeats`, `--variants`, `--libraries`, `--measure`. Run `python bench.py --help` for the full list with descriptions.

## License

MIT - see [LICENSE](LICENSE) for details.
