Skip to main content

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-elasticsearch and greyhorse-web plug 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] and Maybe[T] with a monadic API, plus an ADT-style Enum with Unit/Tuple/Struct variants 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

Metadata

Release files for greyhorse 0.5.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for greyhorse 0.5.8
File Size Uploaded
greyhorse-0.5.8.tar.gz 2.3 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for greyhorse 0.5.8
File Interpreter ABI Platform
greyhorse-0.5.8-py3-none-any.whl Python 3 none any Details

Total release size: 3.6 MB

Release files / greyhorse-0.5.8.tar.gz

Download URL greyhorse-0.5.8.tar.gz
Size 2.3 MB
Tags Source
SHA-256 checksum
How to use checksums
58afac8da0a99757107265c430c2794f939cea45caadcfbe9b1bb7ad8609047b
BLAKE2b-256 checksum
How to use checksums
a054ce5435dff4a027b2c20479884ed06d6a98fce30c36a60f73bbc231d03597
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Release files / greyhorse-0.5.8-py3-none-any.whl

Download URL greyhorse-0.5.8-py3-none-any.whl
Size 1.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
b1c0d0b3b6f184771ea767268b538a42995ba37d370c9c5bcc284b01dae21985
BLAKE2b-256 checksum
How to use checksums
387e0f2b5acfbc0ea02305c7c70bab8c0bc0870b2ca88b83733c74534ad9d900
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page