One Host, many Entrypoints — a batteries-optional microservice runtime for async Python.
Describe a service once as an AppSpec (DI container, lifecycle, observability, warmup, health) and
run it through any number of pluggable entrypoints — HTTP, gRPC, scheduler, background daemon,
one-shot batch — under one unified lifecycle. The "API vs worker" distinction does not exist at the
core: a cron job and an HTTP request are the same thing — one unit of work in a fresh DI scope.
This is the model behind .NET Generic Host, Spring SmartLifecycle and go-kratos
transport.Server, adapted to async Python.
Tip
Building this with an AI assistant? Hand it one page instead of the whole site: the public API surface, the startup and shutdown ordering that breaks code when it is broken, which batteries are opt-in and silent when they are not switched on, the mistakes models actually make with this API, and a map of which page to fetch for the rest. Every docs page is also served as raw Markdown at its own URL, and a Copy page button at the top of each one hands it straight to a chat window.
- One lifecycle for every archetype: Bootstrap → Warmup → Ready → Serve → Drain → Cleanup.
Kubernetes-correct shutdown out of the box: readiness flips to
falsebefore draining, every entrypoint finishes in-flight work within a grace window, the DI scope closes last, and a service that dies mid-serve exits non-zero instead of looking like a graceful stop. - DI-agnostic two-tier scopes: the core depends on no DI library.
AppScopeholds process-lifetime singletons; a freshUnitScopewraps every request / RPC / job / message. A dishka adapter ships in the box; any container fits by implementing two methods. - Pluggable observability add-ons: metrics / tracing / logging / error-tracking are protocols
in the kernel with selectable, extra-gated backends (prometheus, OpenTelemetry, Sentry,
structlog). Adding a backend = one module + one
register_sinkcall, zero core changes. - One error taxonomy, every transport: a
ServiceErrorraised in business code renders as an RFC 9457 problem document over HTTP and as the mappedgrpc.StatusCodeover gRPC, with one masking rule for non-public details and a pluggable renderer when you own the wire format. - Zero hard dependencies:
pip install servicewrightbrings pure Python. Every framework binding lives behind an extra; the kernel never imports an SDK, a vendor, or a transport.
pip install servicewright # pure kernel, zero dependencies
pip install "servicewright[fastapi]" # + FastAPI/uvicorn entrypoint
pip install "servicewright[grpc]" # + gRPC entrypoint
pip install "servicewright[apscheduler4]" # + cron/scheduler entrypoint
pip install "servicewright[metrics,observability,sentry]" # + prometheus, otel+structlog, sentry
pip install "servicewright[uvloop]" # + uvloop, picked up by run_sync(loop="auto")
pip install "servicewright[all]" # everything except the conflicting [apscheduler3]Requirements: Python 3.12+
from servicewright import AppSpec, ObsConfig, ObservabilityManager, Service, run_sync
from servicewright.adapters.apscheduler4 import ScheduledJob, SchedulerEntrypoint
from servicewright.adapters.fastapi import FastApiEntrypoint
def build_service() -> Service:
spec = AppSpec(
service_name="orders-service",
create_container=build_container, # your DI container factory
observability=ObservabilityManager(
ObsConfig(metrics="prometheus", tracing="otel", logging="structlog"),
),
)
http = FastApiEntrypoint(routers=(router,)) # kind="http"
cron = SchedulerEntrypoint(jobs=[ # kind="scheduler"
ScheduledJob(id="sweep", func=sweep_expired_orders, trigger=interval_trigger),
])
return Service(spec, entrypoints=[http, cron])
if __name__ == "__main__":
run_sync(build_service(), Settings()) # loop="auto": uvloop when installed, asyncio otherwiseBoth entrypoints share one DI container, one observability setup and one graceful shutdown.
Scaling the worker separately later = the same AppSpec in a second process with a different
entrypoint list.
| Archetype | Adapter | Extra |
|---|---|---|
| HTTP API | adapters.fastapi / adapters.litestar |
fastapi / litestar |
| gRPC API | adapters.grpc |
grpc |
| Scheduled / cron | adapters.apscheduler4 / adapters.apscheduler3 |
apscheduler4 / apscheduler3 |
| Background daemon | DaemonEntrypoint (built-in) |
— |
| One-shot / batch | OneShotEntrypoint (built-in) |
— |
Writing your own entrypoint = implementing four methods (bind, serve, drain, stop)
with nothing installed.
The kernel is core/; everything that touches a third-party SDK is an extra-gated adapter. An
import-linter contract enforces the direction in CI: deleting adapters/ leaves core/ importable.
| Module | Responsibility | Extra |
|---|---|---|
servicewright |
AppSpec, Service, Host, run, run_sync — the public vocabulary |
— |
core.contracts |
Entrypoint, Plugin, container/settings/health protocols |
— |
core.aio.host |
The lifecycle kernel: warmup → ready → serve → drain → cleanup | — |
core.errors |
ServiceError, ErrorKind, RFC 9457 renderer + renderer seam |
— |
core.context |
Transport-neutral correlation store + outbound propagation | — |
core.health |
HealthRegistry driving both HTTP routes and the gRPC health service |
— |
core.warmup |
Priority-grouped, fail-fast warmup before readiness flips | — |
core.observability |
Sink protocols, NullObjects, backend registry, redaction | — |
adapters.builtin |
DaemonEntrypoint, OneShotEntrypoint — zero-dependency |
— |
adapters.fastapi |
FastAPI entrypoint, middleware stack, problem-details handlers | fastapi |
adapters.litestar |
Litestar entrypoint | litestar |
adapters.grpc |
gRPC entrypoint over grpc-server-kit, error mapping, health bridge | grpc |
adapters.apscheduler4 / apscheduler3 |
Scheduler entrypoints with identical public surfaces | apscheduler4 / apscheduler3 |
adapters.dishka |
dishka ⇄ core scope binding | dishka |
adapters.settings |
pydantic-settings models of the settings contract | settings |
adapters.observability |
prometheus / OpenTelemetry / Sentry / structlog / stdlib sinks | see below |
adapters.warmers, adapters.health |
Redis / Postgres / Kafka warmers and checks | redis, postgres, kafka |
servicewright.testing |
FakeContainer, FakeEntrypoint, FakeScope, FakeSettings |
— |
| Extra | Pulls in | Enables |
|---|---|---|
fastapi |
fastapi, uvicorn, deadline-budget, prometheus-fastapi-instrumentator | FastApiEntrypoint + its middleware stack |
litestar |
litestar, uvicorn | LitestarEntrypoint |
grpc |
grpc-server-kit[reflection,channelz,health] | GrpcEntrypoint, error mapping, health bridge |
apscheduler4 / apscheduler3 |
apscheduler 4.x / 3.x | SchedulerEntrypoint (one major per environment) |
dishka |
dishka | DishkaContainer |
settings |
pydantic-settings | BaseServiceSettings + the four section models, with the sinks' defaults |
observability |
opentelemetry-sdk, OTLP gRPC exporter, structlog | otel tracing + structlog logging sinks |
fastapi-tracing |
the above + opentelemetry-instrumentation-fastapi | HTTP request spans |
metrics |
prometheus-client | prometheus metrics sink + /system/metrics |
sentry |
sentry-sdk | sentry error-tracking sink |
redis / postgres / kafka |
redis / sqlalchemy / aiokafka | matching warmers and health checks |
uvloop |
uvloop | the uvloop event loop, picked by run_sync(loop="auto" | "uvloop") |
all |
everything except apscheduler3 |
the full runtime |
Runnable, self-contained scripts (each exits 0):
examples/minimal_service.py— the smallest real service, narrating every lifecycle phase in order.examples/http_service.py— a real uvicorn server with dishka DI, per-request scopes, health probes, an RFC 9457 error on the wire and a graceful drain.examples/errors_and_context.py— the error taxonomy, a custom renderer, masking, and the correlation store with outbound propagation.examples/warmup_and_health.py— warmup priority groups, fail-fast, and health checks driving readiness.
Full documentation: bedrock-python.github.io/servicewright
| For AI agents | the whole library on one page, written for a coding assistant |
| Your first service | a complete service with nothing installed but the kernel |
| Tutorial | an HTTP API and a cron job in one process |
| Architecture | the six nouns, the two layers, the dependency rule |
| Lifecycle | phase order, budgets, signals, exit codes |
| Adapters | FastAPI, Litestar, gRPC, scheduler, dishka, observability backends |
| Blueprints | copy-paste skeletons: project layout, HTTP API, gRPC, worker, batch job |
| Writing an entrypoint | four methods, worked end to end |
| Kubernetes | probes, grace periods, exit codes |
| Runbooks | symptom → cause → fix |
| API reference | generated from the source |
The design source-of-truth lives in ARCHITECTURE.md.
Apache 2.0 — see LICENSE.