Metadata-Version: 2.5
Name: grelmicro
Version: 0.37.2
Summary: grelmicro is a lightweight toolkit for building Python applications that need to coordinate work across processes
Project-URL: Documentation, https://grelmicro.grel.info
Project-URL: Repository, https://github.com/grelinfo/grelmicro.git
Project-URL: Issues, https://github.com/grelinfo/grelmicro/issues
Author-email: Loïc Gremaud <grelinfo@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: async,asyncio,cache,circuit-breaker,cloud-native,containerized,distributed-lock,distributed-systems,fastapi,health-checks,kubernetes,leader-election,logging,microservice,microservice-patterns,modular-monolith,opentelemetry,postgresql,pydantic,rate-limiter,redis,self-contained-systems,task-scheduler,tracing
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: AsyncIO
Classifier: Framework :: FastAPI
Classifier: Framework :: Pydantic
Classifier: Framework :: Pydantic :: 2
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: fast-depends>=2.0.0
Requires-Dist: pydantic-settings>=2.5.0
Requires-Dist: pydantic>=2.5.0
Provides-Extra: faststream
Requires-Dist: faststream>=0.7.0; extra == 'faststream'
Provides-Extra: instrumentation
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.41b0; extra == 'instrumentation'
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == 'instrumentation'
Requires-Dist: opentelemetry-instrumentation-redis>=0.41b0; extra == 'instrumentation'
Provides-Extra: kubernetes
Requires-Dist: lightkube>=0.15.0; extra == 'kubernetes'
Provides-Extra: opentelemetry
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-exporter-prometheus>=0.41b0; extra == 'opentelemetry'
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'opentelemetry'
Requires-Dist: prometheus-client>=0.19.0; extra == 'opentelemetry'
Provides-Extra: postgres
Requires-Dist: asyncpg>=0.30.0; extra == 'postgres'
Provides-Extra: redis
Requires-Dist: redis>=5.0.0; extra == 'redis'
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.20.0; extra == 'sqlite'
Provides-Extra: standard
Requires-Dist: orjson>=3.10.11; extra == 'standard'
Requires-Dist: uvloop>=0.21.0; (sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy') and extra == 'standard'
Provides-Extra: structlog
Requires-Dist: orjson>=3.10.11; extra == 'structlog'
Requires-Dist: structlog>=24.1.0; extra == 'structlog'
Provides-Extra: valkey
Requires-Dist: valkey>=6; extra == 'valkey'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6; extra == 'yaml'
Description-Content-Type: text/markdown

<p align="center">
  <a href="https://grelmicro.grel.info">
    <img alt="grelmicro" class="grel-wordmark" src="https://raw.githubusercontent.com/grelinfo/grelmicro/main/docs/img/logo/wordmark.svg" width="360">
  </a>
</p>

<p align="center">
  <em>Async-first toolkit. Microservice patterns inside.</em>
</p>

<p align="center">
  A Python toolkit for distributed systems: microservices, modular monoliths, and self-contained systems.
</p>

<p align="center">
  <a href="https://pypi.org/project/grelmicro/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/grelmicro"></a>
  <a href="https://pypi.org/project/grelmicro/"><img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/grelmicro"></a>
  <a href="https://github.com/grelinfo/grelmicro/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
  <a href="https://codecov.io/gh/grelinfo/grelmicro"><img alt="codecov" src="https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR"></a>
  <a href="https://github.com/astral-sh/uv"><img alt="uv" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json"></a>
  <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
  <a href="https://github.com/astral-sh/ty"><img alt="ty" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json"></a>
  <a href="https://securityscorecards.dev/viewer/?uri=github.com/grelinfo/grelmicro"><img alt="OpenSSF Scorecard" src="https://api.securityscorecards.dev/projects/github.com/grelinfo/grelmicro/badge"></a>
  <a href="https://www.bestpractices.dev/projects/13655"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/13655/badge"></a>
  <a href="https://slsa.dev/spec/latest/levels"><img alt="SLSA Build Level 2" src="https://slsa.dev/images/gh-badge-level2.svg"></a>
</p>

<p align="center">
  <img alt="A FastAPI route protected by a grelmicro rate limiter and health check" src="https://raw.githubusercontent.com/grelinfo/grelmicro/main/docs/img/demo.gif" width="800">
</p>

______________________________________________________________________

**Documentation**: [https://grelmicro.grel.info/](https://grelmicro.grel.info)

**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro)

______________________________________________________________________

## Why grelmicro

grelmicro is an async Python toolkit for microservices and distributed systems: shared locks, caching, rate limits, circuit breakers, retries, and scheduled tasks.

It ships them as small, composable modules with pluggable backends, alongside idempotency keys, the transactional outbox, logging, health checks, metrics, and tracing. Async-first, type-safe, and fully tested.

It is built for any Python application that coordinates work across processes, workers, or replicas. The same primitives serve microservices, a modular monolith, or a self-contained system, and fit naturally into containerized and Kubernetes deployments.

- **Micro**: one focused primitive per module. Each covers a microservice pattern (distributed lock, leader election, rate limiter, circuit breaker, health check API, externalised configuration).
- **Fast**: small footprint by design. We keep the layers thin so your code stays quick.
- **Async-first**: every I/O call is `async` / `await`. Drops into FastAPI, FastStream, and any asyncio-based stack.
- **Backend-agnostic**: each primitive is a protocol. Swap Redis for PostgreSQL or SQLite without touching application code.
- **Railguarded**: fully tested, type-checked, and validated. Pre-1.0 the API may change on a minor release. `1.x` follows standard semver.

grelmicro is **not** a task queue (reach for Celery, Dramatiq, or taskiq) and **not** a web framework (it plugs into FastAPI, Starlette, Litestar, and FastStream). It fills the gap between the web framework you picked and the infrastructure you run.

Already using `aiocache`, `slowapi`, `pybreaker`, `tenacity`, or `aioredlock`? See the [comparison page](https://grelmicro.grel.info/comparison/) for a per-domain breakdown.

## Modules

| Module | Summary |
|---|---|
| [**Cache**](https://grelmicro.grel.info/cache/) | `TTLCache` and a `@cached` decorator with local and distributed stampede protection. Redis, Valkey, PostgreSQL, SQLite, in-memory. |
| [**Idempotency**](https://grelmicro.grel.info/idempotency/) | Idempotency keys that make a retried operation safe. Store the response once, replay it on repeat, single-flight across replicas. |
| [**Coordination**](https://grelmicro.grel.info/coordination/) | Distributed `Lock`, `ReadWriteLock`, `TaskLock`, and `LeaderElection`. Redis, Valkey, PostgreSQL, SQLite, Kubernetes, in-memory. |
| [**Outbox**](https://grelmicro.grel.info/outbox/) | Transactional outbox. `publish` a message inside your database transaction and a background relay delivers it at least once with retries and dead-lettering. PostgreSQL, in-memory. |
| [**Task Scheduler**](https://grelmicro.grel.info/task/) | Interval and cron tasks with durable, distributed at-most-once execution. A modern, lightweight alternative to APScheduler and Celery beat. |
| [**Resilience**](https://grelmicro.grel.info/resilience/) | [Shield](https://grelmicro.grel.info/resilience/shield/), [Circuit Breaker](https://grelmicro.grel.info/resilience/circuit-breaker/), [Rate Limiter](https://grelmicro.grel.info/resilience/rate-limiter/), [Retry](https://grelmicro.grel.info/resilience/retry/), [Timeout](https://grelmicro.grel.info/resilience/timeout/), [Bulkhead](https://grelmicro.grel.info/resilience/bulkhead/), and [Fallback](https://grelmicro.grel.info/resilience/fallback/), with pluggable algorithms and backends. |
| [**Logging**](https://grelmicro.grel.info/logging/) | 12-factor logging with JSON, LOGFMT, TEXT, or PRETTY output, structured error rendering, and OpenTelemetry trace context. |
| [**Tracing**](https://grelmicro.grel.info/tracing/) | Unified instrumentation. `@instrument` creates OpenTelemetry spans and enriches log records with structured context. |
| [**Metrics**](https://grelmicro.grel.info/metrics/) | OpenTelemetry metrics with a `@measure` decorator, a Prometheus `/metrics` router, and built-in instrumentation across components. |
| [**Health**](https://grelmicro.grel.info/health/) | Health checks with concurrent runners and FastAPI liveness / readiness integration. |
| [**Client IP**](https://grelmicro.grel.info/clientip/) | Resolve the real caller behind a reverse proxy, trusting only the `X-Forwarded-For` entries your own proxies appended. |
| [**Configuration**](https://grelmicro.grel.info/config/) | `ExternalConfig` reconfigures live components from a mounted ConfigMap, Secret, or `.env` / JSON / YAML / TOML file. |

## Installation

```bash
pip install grelmicro
```

See the [Installation guide](https://grelmicro.grel.info/installation/) for `uv` and `poetry` commands, plus optional extras for Redis, PostgreSQL, SQLite, Kubernetes, OpenTelemetry, and structlog.

## Example

### Run the demo

Want to see every Pattern running against real Redis and Postgres? The [FastAPI demo](https://github.com/grelinfo/grelmicro/tree/main/examples/fastapi-demo) starts in three commands:

```bash
cd examples/fastapi-demo
docker compose up --wait
open http://localhost:8000/docs
```

It wires a cached endpoint, a rate-limited endpoint, a circuit-breaker-protected endpoint, a distributed lock, a leader-gated task, and `/healthz` / `/readyz` probes. Read [`app.py`](https://github.com/grelinfo/grelmicro/blob/main/examples/fastapi-demo/app.py) to see each one.

### One route, one primitive

The smallest grelmicro program: a FastAPI route protected by a process-local rate limiter. No `Grelmicro(...)`, no Redis, no lifespan.

```python
from fastapi import FastAPI

from grelmicro.providers.memory import MemoryProvider
from grelmicro.resilience import RateLimitExceededError, RateLimiter

app = FastAPI()
api_limiter = RateLimiter.sliding_window(
    "api", limit=100, window=60, backend=MemoryProvider().ratelimiter()
)


@app.get("/ping")
async def ping() -> dict[str, str]:
    try:
        await api_limiter.acquire_or_raise()
    except RateLimitExceededError:
        return {"status": "throttled"}
    return {"status": "ok"}
```

That is the whole thing. Pick a primitive, name it, give it a backend, call it. The memory backend says per-process on purpose. [Make it fleet-wide](#fastapi-with-one-provider) when you need to.

### FastAPI with one provider

To make the rate limiter fleet-wide, put one provider in a `Grelmicro` container and install it into FastAPI. The provider wires a component for every kind it serves, so there is nothing else to list.

```python
from fastapi import FastAPI

from grelmicro import Grelmicro
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import RateLimitExceededError, RateLimiter

redis = RedisProvider("redis://localhost:6379/0")
micro = Grelmicro(uses=[redis])

api_limiter = RateLimiter.sliding_window("api", limit=100, window=60)

app = FastAPI()
micro.install(app)


@app.get("/ping")
async def ping() -> dict[str, str]:
    try:
        await api_limiter.acquire_or_raise()
    except RateLimitExceededError:
        return {"status": "throttled"}
    return {"status": "ok"}
```

Adding more primitives is the same shape: they resolve through the same provider. `micro.install(app)` opens the app on startup, closes it on shutdown, and lets request handlers resolve backends without passing `backend=`.

### FastAPI integration

Create a file `main.py` with:

```python
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request

from grelmicro import Grelmicro
from grelmicro.cache import JsonSerializer, TTLCache, cached
from grelmicro.clientip import TrustedProxies, resolve_client_address
from grelmicro.health import HealthChecks
from grelmicro.log import configure as configure_logging
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import (
    CircuitBreaker,
    RateLimitExceededError,
    RateLimiter,
)
from grelmicro.coordination import LeaderElection, Lock, TaskLock
from grelmicro.task import Tasks

logger = logging.getLogger(__name__)

# === grelmicro app: one container, one lifespan ===
tasks = Tasks()
health = HealthChecks()

# One line says where the shared state lives.
redis = RedisProvider("redis://localhost:6379/0")

leader = LeaderElection("leader-election")
tasks.add_task(leader)

micro = Grelmicro(uses=[redis, tasks, health])

# === Patterns declared once at module load, no backend wiring ===
ttl_cache = TTLCache(ttl=300, serializer=JsonSerializer())
lock = Lock("shared-resource")
cb = CircuitBreaker("my-service")
api_limiter = RateLimiter.sliding_window("api", limit=100, window=60)


# === FastAPI ===
@asynccontextmanager
async def lifespan(app):
    configure_logging()
    yield


app = FastAPI(lifespan=lifespan)
micro.install(app)


# --- Cache: avoid redundant database queries ---
@cached(ttl_cache)
async def get_user(user_id: int) -> dict:
    return {"id": user_id, "name": "Alice"}


@app.get("/users/{user_id}")
async def read_user(user_id: int):
    return await get_user(user_id)


# --- Circuit Breaker: protect calls to an unreliable service ---
@app.get("/")
async def read_root():
    async with cb:
        return {"Hello": "World"}


# --- Rate Limiter: protect endpoints from overload ---
# Behind a proxy, `request.client.host` is the proxy, so every caller would
# share one bucket. Resolve the real client instead, trusting only your own
# proxies. Drop the `trusted`/`client_key` lines if nothing fronts the app.
trusted = TrustedProxies(["10.0.0.0/8"])


def client_key(request: Request) -> str:
    client = resolve_client_address(request.scope, trusted)
    return client.key if client else "unknown"


@app.get("/api")
async def api_endpoint(request: Request):
    try:
        await api_limiter.acquire_or_raise(key=client_key(request))
    except RateLimitExceededError as exc:
        raise HTTPException(
            status_code=429,
            detail="Too many requests",
            headers={"Retry-After": str(int(exc.retry_after))},
        )
    return {"status": "ok"}


# --- Distributed Lock: synchronize access to a shared resource ---
@app.get("/protected")
async def protected():
    async with lock:
        return {"status": "ok"}


# --- Interval Task: run locally on every worker ---
@tasks.every(seconds=5)
def heartbeat():
    logger.info("heartbeat")


# --- Distributed Task: run once per interval across all workers ---
@tasks.every(seconds=60, lock=TaskLock(lease_duration=300))
def cleanup():
    logger.info("cleanup")


# --- Leader-gated Task: only the leader executes ---
@tasks.every(seconds=10, leader=leader)
def leader_only_task():
    logger.info("leader task")
```

The key shape:

- **One container, one lifespan.** `Grelmicro(uses=[...])` lists every Component and active manager. `async with micro:` opens them all in order, closes in reverse.
- **One Provider, many Components.** `Grelmicro(uses=[redis])` registers a default Component for every kind the `RedisProvider` serves, and they all share its pool. Name a Component only to override one kind: `Grelmicro(uses=[redis, Cache(postgres)])` keeps the rest on Redis.
- **Patterns are declared at module load.** `Lock("cart")`, `TTLCache(ttl=60)`, `CircuitBreaker("svc")` carry no backend reference. They resolve through the active app inside `async with`, and `GrelmicroMiddleware` extends that scope to request handlers. The same `Lock` works in production with Redis and in tests with `MemoryLockAdapter`, no rewiring.
- **Pay only for what you import.** `import grelmicro` does not pull in `redis`, `psycopg`, or any other vendor SDK. First-party Providers live under `grelmicro.providers.{vendor}` and load only when you import them.

For multiple Redis instances, separate names, or test overrides, see the [docs](https://grelmicro.grel.info/).

## Contributing

Report bugs and request features in [GitHub issues](https://github.com/grelinfo/grelmicro/issues/new/choose). The [reporting guide](https://github.com/grelinfo/grelmicro/blob/main/CONTRIBUTING.md#reporting-a-bug) lists what to include and what happens after you file.

To contribute code or docs, read the [contributing guide](https://github.com/grelinfo/grelmicro/blob/main/CONTRIBUTING.md). It explains the pull request process and the requirements for acceptable contributions: the development setup, the code style, and the pre-merge checklist.

Report security issues privately through the [security policy](https://github.com/grelinfo/grelmicro/security/policy), not a public issue.

## License

This project is licensed under the terms of the [MIT license](https://github.com/grelinfo/grelmicro/blob/main/LICENSE).
