Plugins
grelmicro discovers Providers and Adapters through entry-point groups. A third-party package registers under these groups and resolves by short name, so grelmicro never has to depend on the vendor. First-party Providers and Adapters use the very same path: there is no special case.
The three groups
| Group | Maps | Example |
|---|---|---|
grelmicro.providers |
a vendor short name to a Provider class |
redis = "grelmicro.providers.redis:RedisProvider" |
grelmicro.{kind}.adapters |
a short name to an Adapter class for one component kind | redis = "grelmicro.coordination.redis:RedisLockAdapter" |
grelmicro.integrations |
a web framework's top-level module name to its integration module | fastapi = "grelmicro.integrations.fastapi" |
A Provider covers the vendor axis: one Provider per vendor. An Adapter covers
the algorithm axis within a kind, so several adapters can share one Provider
(a Redis lock and a Redis cache both run on RedisProvider).
The component kinds are coordination, coordination.election, coordination.schedule, cache,
ratelimiter, and circuitbreaker.
Publish a third-party integration
micro.install(app) resolves the framework through grelmicro.integrations.
The key is the framework's top-level module name, and the lookup walks the app
class's MRO, so a FastAPI subclass declared in your own package still
matches on fastapi. Only the matching module is imported, so install never
loads a framework the app does not use.
An integration module exposes two functions:
def install(app, micro, *, ambient: bool = True) -> None: ...
def is_bound(app) -> bool: ...
install opens micro alongside the framework's own lifecycle and adds the
per-handler binding. is_bound reports whether that binding is present, which
is what micro.check_ambient_binding(app) and micro.describe(app) read to
catch a forgotten install.
Declare it the same way as a Provider:
[project.entry-points."grelmicro.integrations"]
sanic = "grelmicro_sanic:integration"
Publish a third-party adapter
Say you ship grelmicro-mongo with a Mongo-backed lock. Write the Provider
and the Adapter, then declare them in your package's pyproject.toml:
[project.entry-points."grelmicro.providers"]
mongo = "grelmicro_mongo:MongoProvider"
[project.entry-points."grelmicro.coordination.adapters"]
mongo = "grelmicro_mongo:MongoLockAdapter"
Once your package is installed alongside grelmicro, the name mongo resolves
through the same loader grelmicro uses for its own backends. Users wire it up
exactly like a first-party backend:
from grelmicro import Grelmicro
from grelmicro.coordination import Coordination
from grelmicro_mongo import MongoProvider
mongo = MongoProvider("mongodb://localhost:27017")
micro = Grelmicro(uses=[Coordination(mongo)])
A worked skeleton lives in
examples/third-party-adapter/.
What grelmicro promises your adapter
These rules say how the protocols may change, so an adapter written today keeps working.
An unsupported algorithm raises. RateLimiterBackend.bind and
CircuitBreakerBackend.bind receive a config from a union that grows as new
algorithms land. Your backend supports the kinds it knows and ends the match
by raising NotImplementedError naming the kind it was handed. Never fall
through to a default: a silent one turns a config the operator asked for into
a different algorithm running in production. Dispatch on the kind before you
touch the client, so an unsupported kind fails without a connection.
Result tuples grow by name. RateLimitResult and CircuitBreakerSnapshot
may gain fields with defaults. Read them by attribute, never by unpacking the
whole tuple, or a new field breaks your call site.
Integration signatures are frozen. grelmicro never adds an argument to
install(app, micro, *, ambient=True) or is_bound(app). A new capability
arrives as a new optional module attribute that grelmicro feature-detects, so
an older integration keeps loading.
ClockBackend is complete. It stays at monotonic and sleep.
Wall-clock time is out of scope, which is why a cron schedule reads the system
clock directly rather than through a backend.
Capture the event loop
Lock, schedule, cache, and circuit-breaker backends must capture the running
loop on __aenter__ and keep it in a _loop attribute:
async def __aenter__(self) -> Self:
self._loop = asyncio.get_running_loop()
return self
The sync adapters (Lock.from_thread, TaskLock.from_thread, the sync
@cached wrapper, CircuitBreaker.from_thread) dispatch coroutines back into
that loop from a worker thread. The protocols declare _loop, so a type
checker reports an adapter that omits it. Set it to None in __init__ and
assign the real loop in __aenter__.
How resolution works
Listing entry points never imports the target module. The module loads only
when a name is resolved, so installing many vendor packages stays cheap. An
unknown name raises ProviderNotRegisteredError or AdapterNotRegisteredError
with the requested name and the names that are installed:
No coordination adapter registered as 'mongo' in the
'grelmicro.coordination.adapters' entry-point group. Available: kubernetes,
memory, postgres, redis, sqlite. Install the package that ships it, or check
the name.