A Signal-based reactive component microkernel for Python backend services
Project description
SignalPy Kernel
A Signal-based reactive component microkernel for Python backend services.
The kernel is three reactive primitives — Signal, Computed, Effect — plus component wiring. Everything else (config, logging, credentials, storage, REST, MCP, CLI) is just components built on top.
Disclosure. Built with Claude's help. The author hopes it lands somewhere between "trash" and "god code" — and is actively asking Python folks who know reactive systems, DI containers, or microkernels to tell them which mistakes were made. Reviews welcome via Issues or Discussions.
Install
pip install signalpy-kernel # core kernel only (zero deps)
pip install "signalpy-kernel[all]" # + providers + REST/CLI + tracing
60-second example
import asyncio
from pydantic import BaseModel
from signalpy.kernel import (
Kernel, component, provides, requires, runnable, lifecycle, computed, effect,
)
from signalpy.providers.config import ConfigProvider
from signalpy.providers.logging_provider import LoggingProvider
class GreetParams(BaseModel):
name: str = "world"
@component("greeter", version="1.0")
@provides("IGreeter")
@requires(config="IConfig")
class Greeter:
@lifecycle.activate
def activate(self):
pass # self.rt.config, self.rt.logger, etc. now available
@computed
def prefix(self):
# Cached. Auto-recomputes when config changes.
return self.rt.config.get("greeter.prefix", "Hello")
@effect
def on_prefix_change(self):
# Auto-tracks deps. Re-runs when they change.
print(f"prefix is now: {self.rt.config.get('greeter.prefix')}")
@runnable("greet", params=GreetParams, description="Greet someone by name")
async def greet(self, params):
return {"message": f"{self.prefix()}, {params.name}!"}
async def main():
kernel = Kernel()
kernel.discover([ConfigProvider, LoggingProvider, Greeter])
await kernel.boot()
result = await kernel.bus.invoke("greeter.greet", {"name": "Alice"})
print(result) # {"message": "Hello, Alice!"}
# Change config — the @effect re-runs automatically.
kernel.registry.require("IConfig").set("greeter.prefix", "Howdy")
await kernel.shutdown()
asyncio.run(main())
The same @runnable is automatically exposed as a REST endpoint, MCP tool, or
CLI command depending on which transport adapter you discover. Components never
know which transport serves them.
What makes it different
-
Reactivity is the foundation, not a layer on top. Every injected service is a
Signal. Readingself.rt.configinside an@effector@computedis a tracked read — when config changes, the effect re-runs automatically. No manual callbacks, no@on_change, no re-injection hacks. -
12 decorators total.
@component,@provides,@requires,@computed,@effect,@lifecycle.*,@runnable,@api,@subscribe,@kind,@skill,@prop. That's the whole API surface. -
Two-axis architecture. Axis 1 (the kernel) is irreplaceable mechanism: ~2,600 LOC across 9 files, zero required dependencies. Axis 2 is replaceable vocabulary: config, logging, credentials, storage, REST/MCP/CLI transports — all just components. The kernel is small enough to read in one sitting.
-
Same
@runnable→ multiple transports.@api("rest", ...)exposes an HTTP endpoint,@api("mcp")exposes an MCP tool. Auth is enforced at the bus level, identical regardless of transport.
Documentation
The full guided tour is at https://bayeslearner.github.io/signalpy-kernel/:
- Tutorials — first component → give-and-take → dynamic services → runnables → gateway → auth → building a provider
- Concepts — architecture, reactivity by example, line-by-line annotated reactive engine, threading model, deployment scales
- Patterns — reactive-intent recipes (
batch,is_stale,cancel_on_supersede, cross-thread writes, mutate-in-place, first-run, cleanup), secret rotation, A/B testing, multi-tenant, hot code update, more - Reference — traits (L0–L3), all 13 decorators, contracts, kernel API
Project layout
src/signalpy/
├── kernel/ Axis 1 — the irreplaceable core (~2,600 LOC, 9 files)
│ ├── reactive.py Signal, Computed, Effect, batch
│ ├── component.py 13 decorators + metadata
│ ├── runtime.py ReactiveRuntime: Signal-backed injection
│ ├── registry.py ServiceRegistry: provide/require + ref counting
│ ├── bus.py Bus: invoke / publish / subscribe
│ ├── lifecycle_manager.py Dependency-ordered activation, effect lifecycle
│ ├── traits.py L0–L3 trait system
│ └── contracts.py Protocol interfaces (IConfig, ILogger, IStorage, …)
│
├── providers/ Axis 2 — platform components (config, logging,
│ credentials, storage, auth, tracing, gateway, …)
├── adapters/ Axis 2 — transport adapters (REST/FastAPI, MCP, CLI/Click)
├── examples/ Progressive examples 01–07
└── tests/ ~290 tests
Constitution (the non-negotiable rules)
- Everything is a component. No privileged subsystems.
- Components give and take. No globals, singletons, or ambient state.
- The kernel has zero business logic.
- Transport is an adapter, never a core concern.
- Distribution can be transparent — the bus is designed for pluggable transports.
- Apps are deployment units, components are composition units.
- Lifecycle is explicit and managed.
- Every API is transport-agnostic.
- The kernel is small. Readable in one sitting.
Inspiration
- Vue 3 / Preact Signals / SolidJS — the Signal/Computed/Effect reactive model
- iPOPO — OSGi-style component lifecycle for Python
- Dapr — building blocks as pluggable components
- Engin / Uber Fx — give-and-take dependency injection
License
MIT — see LICENSE.
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 signalpy_kernel-0.2.0.tar.gz.
File metadata
- Download URL: signalpy_kernel-0.2.0.tar.gz
- Upload date:
- Size: 302.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42a109b7cad9374178e3b65fbeb5ed09fc18202e87c649caecdf26d20278b44b
|
|
| MD5 |
2b1b655f6ddf78e6f6529d6bef435029
|
|
| BLAKE2b-256 |
bf99389aa2732a40c7372ccc6be337aa9bad8b2e049a29e666dd29598ca53595
|
Provenance
The following attestation bundles were made for signalpy_kernel-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on bayeslearner/signalpy-kernel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
signalpy_kernel-0.2.0.tar.gz -
Subject digest:
42a109b7cad9374178e3b65fbeb5ed09fc18202e87c649caecdf26d20278b44b - Sigstore transparency entry: 1441956592
- Sigstore integration time:
-
Permalink:
bayeslearner/signalpy-kernel@ca27af675ed16622bf4f2728dff84b5d77cba591 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bayeslearner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@ca27af675ed16622bf4f2728dff84b5d77cba591 -
Trigger Event:
push
-
Statement type:
File details
Details for the file signalpy_kernel-0.2.0-py3-none-any.whl.
File metadata
- Download URL: signalpy_kernel-0.2.0-py3-none-any.whl
- Upload date:
- Size: 165.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13bbaa9ee9af44490c9bf8158e8b688c9140d67eb259bd21ac8900f0a708d997
|
|
| MD5 |
2e204e95c70012b58fc4fbb5849b8f76
|
|
| BLAKE2b-256 |
08e33f10192b130f042251210ccdd84762e3b4d362965c0355789f02a6dbfaf3
|
Provenance
The following attestation bundles were made for signalpy_kernel-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on bayeslearner/signalpy-kernel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
signalpy_kernel-0.2.0-py3-none-any.whl -
Subject digest:
13bbaa9ee9af44490c9bf8158e8b688c9140d67eb259bd21ac8900f0a708d997 - Sigstore transparency entry: 1441956676
- Sigstore integration time:
-
Permalink:
bayeslearner/signalpy-kernel@ca27af675ed16622bf4f2728dff84b5d77cba591 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bayeslearner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@ca27af675ed16622bf4f2728dff84b5d77cba591 -
Trigger Event:
push
-
Statement type: