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.kernel.contracts import IConfig
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}!"}
# Consumer uses @requires — direct method call, no bus.invoke
@component("app", version="1.0")
@requires(greeter="IGreeter")
class App:
async def run(self):
result = await self.rt.greeter.greet(GreetParams(name="Alice"))
print(result) # {"message": "Hello, Alice!"}
async def main():
kernel = Kernel()
kernel.discover([ConfigProvider, LoggingProvider, Greeter, App])
await kernel.boot()
# Change config — the @effect re-runs automatically.
kernel.registry.require("IConfig").set("greeter.prefix", "Howdy")
await kernel.shutdown()
asyncio.run(main())
@runnable declares the operation schema. Transport adapters (REST, MCP, CLI)
discover schemas via kernel.runnables() and call schema.handler directly.
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. -
11 decorators total.
@component,@provides,@requires,@computed,@effect,@lifecycle.*,@runnable,@subscribe,@kind,@skill,@prop. That's the whole API surface. -
Two-axis architecture. Axis 1 (the kernel) is irreplaceable mechanism: ~3,800 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. Transport adapters discover@runnableschemas and expose them as REST endpoints, MCP tools, or CLI commands. Per-runnabletransports=[]controls visibility. Auth requirements are declared on the schema and enforced by each consumer.
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 11 decorators, contracts, kernel API
Project layout
src/signalpy/
├── kernel/ Axis 1 — the irreplaceable core (~3,800 LOC, 9 files)
│ ├── reactive.py Signal, Computed, Effect, batch
│ ├── component.py 11 decorators + metadata
│ ├── runtime.py ReactiveRuntime: Signal-backed injection
│ ├── registry.py ServiceRegistry: provide/require + ref counting
│ ├── bus.py Event bus: 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/ 341 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 — contracts hide location.
- 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.4.0.tar.gz.
File metadata
- Download URL: signalpy_kernel-0.4.0.tar.gz
- Upload date:
- Size: 296.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11c6c1d646dca7b61e4bf9202c05ec2166bd7f6f6295c811436e19979b552313
|
|
| MD5 |
a291eb859e4fad9935146b05d743e53c
|
|
| BLAKE2b-256 |
e72768a4cbf25d35a3e17b19c24c470cb50494d8a04eb9589457865c2fb321be
|
Provenance
The following attestation bundles were made for signalpy_kernel-0.4.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.4.0.tar.gz -
Subject digest:
11c6c1d646dca7b61e4bf9202c05ec2166bd7f6f6295c811436e19979b552313 - Sigstore transparency entry: 1444667730
- Sigstore integration time:
-
Permalink:
bayeslearner/signalpy-kernel@1f365dbe3cf081a5d55c4e0e5c99450b6b218c12 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/bayeslearner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@1f365dbe3cf081a5d55c4e0e5c99450b6b218c12 -
Trigger Event:
push
-
Statement type:
File details
Details for the file signalpy_kernel-0.4.0-py3-none-any.whl.
File metadata
- Download URL: signalpy_kernel-0.4.0-py3-none-any.whl
- Upload date:
- Size: 161.7 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 |
f9dfa7198fdd2152dbe957cef42a4e010d5e11fd120eaaaac19fdbfb53e0e377
|
|
| MD5 |
283935851251a7be74860e456238c0e0
|
|
| BLAKE2b-256 |
f04feaa17ad4927bb8e67e1c638b87e9bae595db083ca7f46f28e1b1d75f39c8
|
Provenance
The following attestation bundles were made for signalpy_kernel-0.4.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.4.0-py3-none-any.whl -
Subject digest:
f9dfa7198fdd2152dbe957cef42a4e010d5e11fd120eaaaac19fdbfb53e0e377 - Sigstore transparency entry: 1444667873
- Sigstore integration time:
-
Permalink:
bayeslearner/signalpy-kernel@1f365dbe3cf081a5d55c4e0e5c99450b6b218c12 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/bayeslearner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@1f365dbe3cf081a5d55c4e0e5c99450b6b218c12 -
Trigger Event:
push
-
Statement type: