Skip to main content

grelmicro is a lightweight toolkit for building Python applications that need to coordinate work across processes

Reason this release was yanked:

Abandoned 1.0 prerelease line, continuing on 0.x.

Project description

grelmicro

Async-first toolkit. Microservice patterns inside.

A Python toolkit for distributed systems: microservices, modular monoliths, and self-contained systems.

PyPI - Version PyPI - Python Version License: MIT codecov uv Ruff ty OpenSSF Scorecard

A FastAPI route protected by a grelmicro rate limiter and health check

Project status: 1.0 beta. The 1.0 line is feature-complete and its public API is frozen behind a snapshot guard. Prereleases install explicitly: pip install --pre grelmicro (stable installs stay on 0.27 until 1.0.0 final). Breaking changes can still land between betas if testing finds a flaw. After 1.0.0, standard semver applies. See the versioning policy.


Documentation: https://grelinfo.github.io/grelmicro/

Source Code: https://github.com/grelinfo/grelmicro


Why grelmicro

grelmicro ships microservice patterns as small, composable modules with pluggable backends: locks, rate limits, circuit breakers, cache, logging, health checks, and task scheduling. 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, or Litestar). 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 for a per-domain breakdown.

Modules

Module Summary
Cache @cached decorator with local and distributed stampede protection. In-memory TTLCache or RedisCacheAdapter.
Idempotency Idempotency keys that make a retried operation safe. Store the response once, replay it on repeat, single-flight across replicas.
Coordination Distributed Lock, TaskLock, and LeaderElection. Redis, PostgreSQL, SQLite, Kubernetes, in-memory.
Task Scheduler Interval and cron tasks with durable, distributed at-most-once execution. A modern, lightweight alternative to APScheduler and Celery beat.
Resilience Circuit Breaker and Rate Limiter with pluggable algorithms (TokenBucketConfig, SlidingWindowConfig).
Logging 12-factor logging with JSON, LOGFMT, TEXT, or PRETTY output, structured error rendering, and OpenTelemetry trace context.
Tracing Unified instrumentation. @instrument creates OpenTelemetry spans and enriches log records with structured context.
Health Health check registry with concurrent runners and FastAPI liveness / readiness integration.

Installation

pip install grelmicro

See the Installation guide 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 starts in three commands:

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

from fastapi import FastAPI

from grelmicro.resilience import (
    MemoryRateLimiterAdapter,
    RateLimitExceededError,
    RateLimiter,
)

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


@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 adapter says per-process on purpose. Swap to a fleet-wide backend later by composing it inside Grelmicro(uses=[RateLimiterRegistry(redis)]) as shown below.

Lifespan with one provider and one component

To make the rate limiter fleet-wide, wrap it in a Grelmicro container with one provider and one component, bind it to FastAPI's lifespan, and add the middleware so request handlers see the app.

from contextlib import asynccontextmanager

from fastapi import FastAPI

from grelmicro import Grelmicro
from grelmicro.integrations.fastapi import GrelmicroMiddleware
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import (
    RateLimitExceededError,
    RateLimiter,
    RateLimiterRegistry,
)

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

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


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with micro:
        yield


app = FastAPI(lifespan=lifespan)
app.add_middleware(GrelmicroMiddleware, micro=micro)


@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: one extra entry in uses=[...]. The full demo below shows what that looks like.

FastAPI integration

Create a file main.py with:

import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request

from grelmicro import Grelmicro
from grelmicro.cache import Cache, JsonSerializer, TTLCache, cached
from grelmicro.integrations.fastapi import GrelmicroMiddleware
from grelmicro.health import HealthChecks
from grelmicro.log import configure as configure_logging
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import (
    CircuitBreaker,
    CircuitBreakerRegistry,
    RateLimitExceededError,
    RateLimiter,
    RateLimiterRegistry,
)
from grelmicro.resilience.circuitbreaker.memory import MemoryCircuitBreakerAdapter
from grelmicro.coordination import Coordination, LeaderElection, Lock, TaskLock
from grelmicro.task import Tasks

logger = logging.getLogger(__name__)

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

redis = RedisProvider("redis://localhost:6379/0")

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

micro = Grelmicro(uses=[
    Coordination(redis),
    Cache(redis),
    RateLimiterRegistry(redis),
    CircuitBreakerRegistry(MemoryCircuitBreakerAdapter()),
    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 lifespan and middleware ===
@asynccontextmanager
async def lifespan(app):
    configure_logging()
    async with micro:
        yield


app = FastAPI(lifespan=lifespan)
app.add_middleware(GrelmicroMiddleware, micro=micro)


# --- 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 ---
@app.get("/api")
async def api_endpoint(request: Request):
    try:
        await api_limiter.acquire_or_raise(key=request.client.host)
    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. Coordination(redis), Cache(redis), RateLimiterRegistry(redis) all share the same RedisProvider pool. List the Components and grelmicro lifecycles the Provider once. Pass a bare Grelmicro(uses=[redis]) to register a default Component per kind the Provider serves.
  • 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.

License

This project is licensed under the terms of the MIT license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

grelmicro-1.0.0b3.tar.gz (1.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

grelmicro-1.0.0b3-py3-none-any.whl (346.5 kB view details)

Uploaded Python 3

File details

Details for the file grelmicro-1.0.0b3.tar.gz.

File metadata

  • Download URL: grelmicro-1.0.0b3.tar.gz
  • Upload date:
  • Size: 1.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for grelmicro-1.0.0b3.tar.gz
Algorithm Hash digest
SHA256 c03c9d97793d96675761b985764c6d6f8c7d02eb7fc4cdee99e77e4d4e1ba89a
MD5 9609c7fb27215e9e6d9282368fde093f
BLAKE2b-256 4f4ec6781c13118e0a42d0187de5378010eff97a9441328661c0d8f410027ec8

See more details on using hashes here.

Provenance

The following attestation bundles were made for grelmicro-1.0.0b3.tar.gz:

Publisher: release.yml on grelinfo/grelmicro

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file grelmicro-1.0.0b3-py3-none-any.whl.

File metadata

  • Download URL: grelmicro-1.0.0b3-py3-none-any.whl
  • Upload date:
  • Size: 346.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for grelmicro-1.0.0b3-py3-none-any.whl
Algorithm Hash digest
SHA256 ec844b04f3df336746c46413d4d7a1f107496c4dd976614986b4807494dae9cd
MD5 e6c3dc33d25f6253127bcbcb9ab83c50
BLAKE2b-256 c48a6f1fbca28913c23184e65419743de89f29581c04cd93477195ad313a7c14

See more details on using hashes here.

Provenance

The following attestation bundles were made for grelmicro-1.0.0b3-py3-none-any.whl:

Publisher: release.yml on grelinfo/grelmicro

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page