Greyhorse — dependency injection and lifecycle for Python services
Greyhorse assembles a service out of the objects you already wrote, keeps them alive for exactly as long as they should live, and takes them down in the mirror of the order they came up.
Your domain code stays plain Python. No base class to inherit, no decorator on your business logic, no container imported into a repository. Everything the library needs to know is stated in declarations that sit beside your code — so any class it builds can also be constructed by hand in a unit test, with no framework present at all.
What it gives you beyond wiring:
- Lifetimes, not just construction. A resource is set up, checked while it runs, repaired when it breaks and torn down in reverse order. That is the library's centre of gravity, and it is what separates it from a container that only hands you instances.
- Boundaries that hold. A component declares what it takes in and what it hands out; nothing else about it is reachable from outside, and nothing else from outside reaches in. The boundary is checked when the graph is planned, not when a request happens to hit it.
- Failures reported at plan time. A missing dependency, an ambiguous one, a lifetime that cannot be honoured — all of it fails before anything is built, naming what is missing and where to declare it.
- Sync and async in one graph. Both live side by side; the library bridges the two rather than making you pick.
- Storage and transport packages on the same shape.
greyhorse-sqla,greyhorse-redis,greyhorse-nats,greyhorse-clickhouse,greyhorse-elasticsearchandgreyhorse-webplug into the same declarations described below.
Install
pip install greyhorse # requires Python 3.14+
A first look
A module declares what it owns; the library builds it, wires it, and takes it down in the exact mirror of the order it came up:
from typing import ClassVar
from greyhorse.strand import Module, Resource, build
class Database:
def __init__(self, dsn: str = 'sqlite://memory') -> None:
self.dsn = dsn
class App(Module):
name = 'minimal'
resources: ClassVar = Resource(Database)
module = build(App)
module.setup()
try:
... # the Database exists, wired and live
finally:
module.teardown()
There is no factory to write: a class is its own factory when its constructor can be satisfied.
Boundaries that actually hold
A component states what it takes in and what it hands out. Nothing else about it is reachable from outside, and nothing else from outside reaches into it:
from greyhorse.rock import Fragment, factory, provider
from greyhorse.strand import Component, Module, Resource, Shared, Use
class CatalogFragment(Fragment):
exports: ClassVar = {PriceList}
db = provider(Shared[Database]) # a demand, filled from outside
prices = factory(PriceList) # no body needed
class CatalogComponent(Component):
fragments: ClassVar = CatalogFragment
imports: ClassVar = Shared[Database]
exports: ClassVar = PriceList
class Shop(Module):
name = 'shop'
resources: ClassVar = Resource(Database)
components: ClassVar = {'catalog': Use(CatalogComponent)}
Shared[X] lends a value read-only to any number of readers; Mut[X] lends it
for mutation to exactly one at a time and refuses a competing borrower loudly
rather than interleaving writes.
What else is in the box
- Borders — an object that watches a resource and repairs it: phases for setup, check, repair and teardown, driven on a tick. Your resource never learns it is being watched.
- Controllers and services over groups —
Control(cls, over=(A, B))puts one supervisor of your own over several resources in a fixed order;Serve(cls, over=(...))gives one admission gate over several providers. - Handlers and gateways — mount an operation onto a transport (HTTP, a clock) without the operation knowing which transport it landed on.
- A scheduler (
greyhorse.river.sched) — one-shot and periodic jobs with reconciliation, retry policies and worker pools. - Result and Maybe —
Result[T, E]andMaybe[T]with a monadic API, plus an ADT-styleEnumwithUnit/Tuple/Structvariants that a mypy plugin checks for exhaustiveness.
Learn it by running it
Everything above is drawn from examples/, which is a set of
runnable programs rather than snippets — each adds exactly one idea to the one
before it:
python examples/01_minimal.py
| Shows | |
|---|---|
01_minimal.py |
one resource, built and torn down |
02_config.py |
configuration reaching a constructor without the class knowing where it came from |
03_component.py |
a component as a boundary: what it takes in, what it hands out |
04_http.py |
the full shape: resource, service, handler, HTTP dispatch |
05_borders.py |
a border watching a resource and repairing it |
06_submodules.py |
one module declaration mounted twice, configured differently each time |
07_functional.py |
handing down a callable instead of a value, matched by signature |
08_scheduler.py |
one-shot and periodic jobs, ticked by hand |
09_controller_service.py |
your own controller and service over a group of resources |
tests/test_examples.py runs every one of them and requires a zero exit code,
so nothing here can quietly rot into documentation that lies.
Packages
greyhorse.strand |
lifecycle and ownership — Module, Component, Controller, Service, gateways, and the borrow windows Shared/Mut |
greyhorse.rock |
declarative wiring — Fragment, @factory, @provider, the resolver |
greyhorse.river |
operations, providers, slots, the scheduler |
greyhorse.data |
data-layer abstractions — repositories, engines, serializers, cache |
greyhorse.result / .maybe / .enum / .error |
core primitives |
Every name a package lists in __all__ is guaranteed to resolve, carries no
private type in its signatures, and is checked in CI — see
tests/test_public_api.py.
Development
uv python pin 3.14
uv venv && uv sync
source .venv/bin/activate
ruff check --unsafe-fixes --fix && ruff format
pytest
License
MIT
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 greyhorse-0.5.5.tar.gz.
File metadata
- Download URL: greyhorse-0.5.5.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72b63578db19986157c08cf9539569abfbcd0f650de1264499a066a5ecab8f76
|
|
| MD5 |
0340c3b0a251b15e4443e4214e38661c
|
|
| BLAKE2b-256 |
92679ed8f3069809f39f69fba1e331410c50baf14c34af021f52886cf89a246d
|
File details
Details for the file greyhorse-0.5.5-py3-none-any.whl.
File metadata
- Download URL: greyhorse-0.5.5-py3-none-any.whl
- Upload date:
- Size: 865.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b13f61eaac7be27e7cbbf361689b46efc716c1e46532de9938ec16f56189ee4
|
|
| MD5 |
e52b6b9ccfc8cd66b1fadf6567813789
|
|
| BLAKE2b-256 |
8a1c717fea49cd53371237fcc66ca8b2afbc3c4242ed40beaa501c16ab32a278
|