Metadata-Version: 2.5
Name: servicewright
Version: 0.4.0
Summary: Batteries-optional microservice runtime: lifecycle, observability, warmup and gRPC/FastAPI transport entities to build services fast
Project-URL: Repository, https://github.com/bedrock-python/servicewright
Project-URL: Documentation, https://bedrock-python.github.io/servicewright/
Project-URL: Changelog, https://github.com/bedrock-python/servicewright/blob/master/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/bedrock-python/servicewright/issues
Author-email: Alex Shalaev <shalaevad.alexey@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: asyncio,fastapi,graceful-shutdown,grpc,lifecycle,microservices,observability,runtime
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Provides-Extra: all
Requires-Dist: aiokafka>=0.10.0; extra == 'all'
Requires-Dist: apscheduler<5,>=4.0.0a5; extra == 'all'
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'all'
Requires-Dist: dishka>=1.4.0; extra == 'all'
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == 'all'
Requires-Dist: grpc-server-kit[channelz,health,reflection]<0.2,>=0.1.0; extra == 'all'
Requires-Dist: litestar>=2.12.0; extra == 'all'
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.39.1; extra == 'all'
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.60b1; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.39.1; extra == 'all'
Requires-Dist: prometheus-client<1.0.0,>=0.24.0; extra == 'all'
Requires-Dist: prometheus-fastapi-instrumentator<9.0.0,>=7.0.0; extra == 'all'
Requires-Dist: redis>=4.2.0; extra == 'all'
Requires-Dist: sentry-sdk<3.0.0,>=2.0.0; extra == 'all'
Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.0; extra == 'all'
Requires-Dist: structlog<27.0.0,>=25.5.0; extra == 'all'
Requires-Dist: uvicorn[standard]<1.0.0,>=0.34.0; extra == 'all'
Provides-Extra: apscheduler3
Requires-Dist: apscheduler<4,>=3.10; extra == 'apscheduler3'
Provides-Extra: apscheduler4
Requires-Dist: apscheduler<5,>=4.0.0a5; extra == 'apscheduler4'
Provides-Extra: dishka
Requires-Dist: dishka>=1.4.0; extra == 'dishka'
Provides-Extra: fastapi
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'fastapi'
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == 'fastapi'
Requires-Dist: prometheus-fastapi-instrumentator<9.0.0,>=7.0.0; extra == 'fastapi'
Requires-Dist: uvicorn[standard]<1.0.0,>=0.34.0; extra == 'fastapi'
Provides-Extra: fastapi-tracing
Requires-Dist: deadline-budget<1.0.0,>=0.1.0; extra == 'fastapi-tracing'
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == 'fastapi-tracing'
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.39.1; extra == 'fastapi-tracing'
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.60b1; extra == 'fastapi-tracing'
Requires-Dist: opentelemetry-sdk>=1.39.1; extra == 'fastapi-tracing'
Requires-Dist: prometheus-fastapi-instrumentator<9.0.0,>=7.0.0; extra == 'fastapi-tracing'
Requires-Dist: structlog<27.0.0,>=25.5.0; extra == 'fastapi-tracing'
Requires-Dist: uvicorn[standard]<1.0.0,>=0.34.0; extra == 'fastapi-tracing'
Provides-Extra: grpc
Requires-Dist: grpc-server-kit[channelz,health,reflection]<0.2,>=0.1.0; extra == 'grpc'
Provides-Extra: kafka
Requires-Dist: aiokafka>=0.10.0; extra == 'kafka'
Provides-Extra: litestar
Requires-Dist: litestar>=2.12.0; extra == 'litestar'
Requires-Dist: uvicorn[standard]<1.0.0,>=0.34.0; extra == 'litestar'
Provides-Extra: metrics
Requires-Dist: prometheus-client<1.0.0,>=0.24.0; extra == 'metrics'
Provides-Extra: observability
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.39.1; extra == 'observability'
Requires-Dist: opentelemetry-sdk>=1.39.1; extra == 'observability'
Requires-Dist: structlog<27.0.0,>=25.5.0; extra == 'observability'
Provides-Extra: postgres
Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.0; extra == 'postgres'
Provides-Extra: redis
Requires-Dist: redis>=4.2.0; extra == 'redis'
Provides-Extra: sentry
Requires-Dist: sentry-sdk<3.0.0,>=2.0.0; extra == 'sentry'
Description-Content-Type: text/markdown

# servicewright

**One `Host`, many `Entrypoint`s** — a batteries-optional microservice runtime for async Python.

[![PyPI](https://img.shields.io/pypi/v/servicewright?color=blue)](https://pypi.org/project/servicewright/)
[![Python](https://img.shields.io/pypi/pyversions/servicewright)](https://pypi.org/project/servicewright/)
[![License](https://img.shields.io/github/license/bedrock-python/servicewright)](LICENSE)
[![CI](https://github.com/bedrock-python/servicewright/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/bedrock-python/servicewright/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/bedrock-python/servicewright/graph/badge.svg)](https://codecov.io/gh/bedrock-python/servicewright)
[![Docs](https://img.shields.io/badge/docs-online-blue)](https://bedrock-python.github.io/servicewright/)

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.

## Why

- **One lifecycle for every archetype**: Bootstrap → Warmup → Ready → Serve → Drain → Cleanup.
  Kubernetes-correct shutdown out of the box: readiness flips to `false` *before* 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. `AppScope` holds
  process-lifetime singletons; a fresh `UnitScope` wraps 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_sink` call, zero core changes.
- **One error taxonomy, every transport**: a `ServiceError` raised in business code renders as an
  RFC 9457 problem document over HTTP and as the mapped `grpc.StatusCode` over gRPC, with one
  masking rule for non-public details and a pluggable renderer when you own the wire format.
- **Zero hard dependencies**: `pip install servicewright` brings pure Python. Every framework
  binding lives behind an extra; the kernel never imports an SDK, a vendor, or a transport.

## Installation

```bash
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[all]"             # everything except the conflicting [apscheduler3]
```

**Requirements:** Python 3.12+

## Quick start — HTTP API + cron in ONE process

```python
import asyncio

from servicewright import AppSpec, ObsConfig, ObservabilityManager, Service, run
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__":
    asyncio.run(run(build_service(), Settings()))
```

Both 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.

## Entrypoints

| 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.

## What's inside

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` — 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.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` | — |

## Optional dependencies

| 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` |
| `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 |
| `all` | everything except `apscheduler3` | the full runtime |

## Examples

Runnable, self-contained scripts (each exits 0):

- [`examples/minimal_service.py`](examples/minimal_service.py) — the smallest real service, narrating
  every lifecycle phase in order.
- [`examples/http_service.py`](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`](examples/errors_and_context.py) — the error taxonomy, a custom
  renderer, masking, and the correlation store with outbound propagation.
- [`examples/warmup_and_health.py`](examples/warmup_and_health.py) — warmup priority groups,
  fail-fast, and health checks driving readiness.

## Documentation

Full documentation: **[bedrock-python.github.io/servicewright](https://bedrock-python.github.io/servicewright/)**

| | |
| --- | --- |
| [Your first service](https://bedrock-python.github.io/servicewright/getting-started/first-service/) | a complete service with nothing installed but the kernel |
| [Tutorial](https://bedrock-python.github.io/servicewright/getting-started/tutorial/) | an HTTP API and a cron job in one process |
| [Architecture](https://bedrock-python.github.io/servicewright/concepts/architecture/) | the six nouns, the two layers, the dependency rule |
| [Lifecycle](https://bedrock-python.github.io/servicewright/concepts/lifecycle/) | phase order, budgets, signals, exit codes |
| [Adapters](https://bedrock-python.github.io/servicewright/adapters/overview/) | FastAPI, Litestar, gRPC, scheduler, dishka, observability backends |
| [Blueprints](https://bedrock-python.github.io/servicewright/blueprints/project-layout/) | copy-paste skeletons: project layout, HTTP API, gRPC, worker, batch job |
| [Writing an entrypoint](https://bedrock-python.github.io/servicewright/guides/custom-entrypoint/) | four methods, worked end to end |
| [Kubernetes](https://bedrock-python.github.io/servicewright/operations/kubernetes/) | probes, grace periods, exit codes |
| [Runbooks](https://bedrock-python.github.io/servicewright/operations/runbooks/) | symptom → cause → fix |
| [API reference](https://bedrock-python.github.io/servicewright/reference/servicewright/) | generated from the source |

The design source-of-truth lives in [ARCHITECTURE.md](ARCHITECTURE.md).

## License

Apache 2.0 — see [LICENSE](LICENSE).
