Skip to content

Providers

A Provider is a first-class connection object. It owns the vendor URL, the native client (a Redis pool, an asyncpg pool, ...), and the lifecycle of both. Components like Sync, Cache, and RateLimit accept a Provider directly and use its canonical adapter under the hood.

Two providers ship today: RedisProvider and PostgresProvider. More will follow.

The canonical shape

Pass a Provider to every Component that needs the same connection:

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import RateLimit
from grelmicro.sync import Sync

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

micro = Grelmicro(uses=[
    redis,
    Sync(redis),
    Cache(redis),
    RateLimit(redis),
])

async with micro:
    ...

Components dispatch to the Provider's factory methods (provider.sync(), provider.cache(), provider.ratelimiter()). The Adapter classes (RedisSyncAdapter, RedisCacheAdapter, RedisRateLimiterAdapter) stay public as escape hatches but rarely appear in user code.

Recipe 1: env-driven

Construct the Provider without arguments and let it read REDIS_* from the environment:

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.providers.redis import RedisProvider
from grelmicro.sync import Sync

redis = RedisProvider()  # reads REDIS_URL or REDIS_HOST + REDIS_PORT + ...

micro = Grelmicro(uses=[
    redis,
    Sync(redis),
    Cache(redis),
])

Set REDIS_URL (or REDIS_HOST + REDIS_PORT + REDIS_DB + REDIS_PASSWORD) in the environment.

Recipe 2: split pools by env prefix

Two Redis instances (or two databases) live behind different prefixes. Each prefix gets its own Provider:

cache_redis = RedisProvider(env_prefix="CACHE_REDIS_")
session_redis = RedisProvider(env_prefix="SESSION_REDIS_")

micro = Grelmicro(uses=[
    cache_redis,
    session_redis,
    Sync(session_redis),
    Cache(cache_redis),
])

Set CACHE_REDIS_URL and SESSION_REDIS_URL (or the decomposed forms). The two components talk to two pools.

Recipe 3: bring your own client

You already own a Redis client (custom retry, sentinel, auth, or a testcontainers fixture). Wrap it with from_client:

import redis.asyncio as redis

from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.providers.redis import RedisProvider

client = redis.Redis(host="prod.cache", socket_timeout=5)
redis_provider = RedisProvider.from_client(client)  # caller owns the client

micro = Grelmicro(uses=[redis_provider, Cache(redis_provider)])

Pass own=True to hand ownership to the provider. It will close the client when the provider exits, useful in pytest fixtures:

@pytest.fixture
async def redis_provider(redis_container):
    async with RedisProvider.from_client(
        redis_container.get_client(), own=True
    ) as provider:
        yield provider

Construction forms

RedisProvider("redis://localhost:6379")      # positional URL
RedisProvider(url="redis://...")             # keyword URL
RedisProvider(host="x", port=6379, db=0)     # decomposed kwargs
RedisProvider()                              # env-driven (REDIS_*)
RedisProvider(env_prefix="CACHE_REDIS_")     # custom env prefix
RedisProvider(env_load=False)                # kwargs only, no env
RedisProvider.from_config(RedisConfig(...))  # from a config object
RedisProvider.from_client(client)            # bring-your-own client

Factory methods

Each Provider exposes factory methods that return its canonical adapter:

Method Returns RedisProvider PostgresProvider
.sync(**kwargs) SyncBackend implementation
.cache(**kwargs) CacheBackend implementation
.ratelimiter(**kwargs) RateLimiterBackend impl
.breaker(**kwargs) CircuitBreakerBackend impl

Factories that do not apply raise NotImplementedError with a message pointing to the right alternative. Sync(provider), Cache(provider), RateLimit(provider), and Breaker(provider) call these factories.

Postgres

PostgresProvider ships the .sync() factory. The provider wraps an asyncpg.Pool and opens it lazily on __aenter__.

from grelmicro import Grelmicro
from grelmicro.providers.postgres import PostgresProvider
from grelmicro.sync import Sync

postgres = PostgresProvider("postgresql://localhost/app")

micro = Grelmicro(uses=[
    postgres,
    Sync(postgres),
])

Set POSTGRES_URL (or POSTGRES_HOST + POSTGRES_PORT + POSTGRES_DB + POSTGRES_USER + POSTGRES_PASSWORD) for env-driven construction.

For two pools (writer + reader), split by env prefix:

write = PostgresProvider(env_prefix="WRITE_POSTGRES_")
read = PostgresProvider(env_prefix="READ_POSTGRES_")

micro = Grelmicro(uses=[
    write,
    read,
    Sync(write),
    Sync(read, name="read"),
])

Construction forms:

PostgresProvider("postgresql://localhost/app")  # positional URL
PostgresProvider(url="postgresql://...")        # keyword URL
PostgresProvider(host="db", port=5432, database="app", user="u", password="pw")
PostgresProvider()                              # env-driven (POSTGRES_*)
PostgresProvider(env_prefix="WRITE_POSTGRES_")  # custom env prefix
PostgresProvider(env_load=False)                # kwargs only, no env
PostgresProvider.from_config(PostgresConfig(...))
PostgresProvider.from_client(pool)              # bring-your-own pool

Lifecycle

The Provider is opened when the Grelmicro app enters and closed when the app exits. Components borrow the Provider's client without managing its lifecycle.

Always list the Provider before the Components that depend on it. uses= opens items in declaration order. PostgresProvider builds its asyncpg.Pool on __aenter__, so a Component placed before its Provider would access provider.client before the pool exists and raise OutOfContextError. Grelmicro.__aenter__ warns on this ordering, but the correct fix is to list the Provider first.

Memory backends

In-memory backends (MemorySyncAdapter, MemoryCacheAdapter, MemoryRateLimiterAdapter, MemoryCircuitBreakerAdapter) have no provider. Pass the adapter directly to its Component:

from grelmicro import Grelmicro
from grelmicro.resilience import Breaker
from grelmicro.resilience.memory import MemoryCircuitBreakerAdapter

micro = Grelmicro(uses=[
    Breaker(MemoryCircuitBreakerAdapter()),
])