RelPrim
Reliability primitives for operations that cross process, network or provider boundaries.
RelPrim helps you wrap external calls with retries, timeouts, fallbacks, validation, circuit breakers, idempotency, provider-aware rate-limit recovery, execution reports and structured events.
Documentation: bartrozycki.com/relprim · Examples
Install
pip install relprim
OpenTelemetry integration is available as an optional extra:
pip install "relprim[otel]"
JSON Schema and Pydantic validator adapters are also optional:
pip install "relprim[jsonschema]"
pip install "relprim[pydantic]"
Wrap an external call in seconds
from relprim import resilient
async def call_gemini(prompt: str) -> str:
return await gemini_client.generate(prompt)
@resilient(retries=3, timeout=10, fallback=call_gemini)
async def call_openai(prompt: str) -> str:
return await openai_client.generate(prompt)
result = await call_openai("Write a short product summary")
print(result.value)
print(result.report.to_dict())
The decorated function returns an OperationResult[T], not a raw value. This keeps the business result and the execution report explicit.
retries=3 allows three retries after the initial attempt, timeout=10 limits
each attempt to ten seconds, and the fallback runs once the retries are
exhausted. If everything fails, OperationExecutionError is raised with the
execution report attached.
RelPrim is fully typed: decorated functions keep their parameters, and type
checkers see OperationResult[str] as the result above.
Why RelPrim?
Most external calls start simple:
response = await openai.chat.completions.create(...)
But production systems need to answer harder questions:
- What if the provider times out?
- What if the response is temporarily unavailable?
- What if the provider returns an invalid response?
- What if the primary provider is down?
- What if you need a fallback provider?
- What if you need to debug what happened after the fact?
RelPrim gives you two levels of adoption.
Beginner-friendly decorator API:
@resilient(retries=3, timeout=10)
async def call_provider(prompt: str) -> str:
return await provider.generate(prompt)
Advanced composition API:
result = await (
async_operation("generate_response", call_provider)
.with_retry(RetryPolicy(max_attempts=3))
.with_timeout(TimeoutPolicy(seconds=10))
.with_validation(validation_policy(...))
.with_fallbacks(fallback_chain(("backup_provider", call_backup)))
.run(prompt)
)
What RelPrim provides
Current primitives:
- Resilient decorator API
- Retry policies
- Exponential backoff with jitter
- Provider-aware rate-limit recovery
- Provider retry-after delay handling
- Async timeout enforcement
- Async fallback chains
- Async circuit breakers
- Validation policies
- Callable validators
- JSON Schema validator adapter
- Pydantic validator adapter
- Structured events
- Event emitters
- No-op event sink
- In-memory event sink
- SQLite event store and persistent event history
- OpenTelemetry event sink
- Async operation builder API
- Structured execution reports
- Operation results
- Typed execution errors
- Idempotency policies
- Concurrent execution joining
- Successful result replay
- In-memory idempotency store
New features are driven by user feedback.
Validate responses with JSON Schema or Pydantic
Plain callable validators are dependency-free. When responses already have a schema or model, optional adapters plug into the same validation policy.
JSON Schema:
from relprim import resilient, validation_policy
from relprim.jsonschema import json_schema_validator
schema = {
"type": "object",
"required": ["id", "name"],
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
}
@resilient(retries=2, validation=validation_policy(json_schema_validator(schema)))
async def fetch_user(user_id: int) -> dict[str, object]:
return await provider.fetch_user(user_id)
Pydantic:
from pydantic import BaseModel
from relprim import resilient, validation_policy
from relprim.pydantic import pydantic_validator
class User(BaseModel):
id: int
name: str
@resilient(retries=2, validation=validation_policy(pydantic_validator(User)))
async def fetch_user(user_id: int) -> dict[str, object]:
return await provider.fetch_user(user_id)
Invalid responses fail with ValidationFailedError and a concise reason such as
'name' is a required property (path: $). Results are validated, never
converted.
See the JSON Schema validation guide and the Pydantic validation guide for details.
Deduplicate repeated calls
RelPrim can deduplicate repeated or concurrent calls using an idempotency key.
from relprim import resilient
@resilient(
retries=2,
timeout=10,
idempotency_key=lambda request_id, amount: f"create-payment:{request_id}",
idempotency_ttl=3600,
)
async def create_payment(
request_id: str,
amount: int,
) -> str:
return await payment_gateway.create(request_id, amount)
The first call executes the operation. Concurrent callers join the same execution, and later calls replay the successful result.
This is not an exactly-once guarantee for external side effects: if a call is cancelled or the process crashes after the side effect but before the result is cached, a later call executes again. For payments and other external mutations, also use the provider's own idempotency key.
The default store is in-memory and single-process. See the idempotency guide for concurrency semantics, key design and store limitations.
Respect provider rate limits
RelPrim can use retry delays supplied by an external provider and avoid waiting longer than the current operation allows.
Provider SDKs expose retry information differently, so a small extractor translates the provider exception into a delay expressed in seconds:
def provider_retry_after(
exception: Exception,
) -> float | None:
if isinstance(exception, ProviderRateLimitError):
return exception.retry_after_seconds
return None
Pass the extractor to @resilient(...):
@resilient(
retries=3,
timeout=10,
rate_limit_on=(ProviderRateLimitError,),
retry_after=provider_retry_after,
max_rate_limit_wait=30,
fallback=call_backup_provider,
)
async def call_provider(prompt: str) -> str:
return await provider.generate(prompt)
RelPrim uses the provider delay when available and falls back to the normal
retry backoff otherwise. When the selected delay exceeds
max_rate_limit_wait, the operation continues into fallback or final failure.
See the rate-limit handling guide for delay selection, report metadata, structured events and limitations.
Persist structured event history
RelPrim can persist structured lifecycle events in a local SQLite database.
from relprim import EventEmitter, SQLiteEventStore, resilient
event_store = SQLiteEventStore("relprim-events.db")
event_emitter = EventEmitter(sinks=(event_store,))
@resilient(
retries=3,
timeout=10,
events=event_emitter,
)
async def call_provider(prompt: str) -> str:
return await provider.generate(prompt)
Stored events can be queried later:
history = await event_store.history(
operation_name="call_provider",
limit=100,
)
See the SQLite event store guide for filtering, pagination, retention and storage limitations.
Export reliability events to OpenTelemetry
RelPrim can export its structured lifecycle events to the currently active OpenTelemetry span.
from relprim import EventEmitter, resilient
from relprim.opentelemetry import OpenTelemetryEventSink
event_emitter = EventEmitter(sinks=(OpenTelemetryEventSink(),))
@resilient(
retries=3,
timeout=10,
events=event_emitter,
)
async def call_provider(prompt: str) -> str:
return await provider.generate(prompt)
The application remains responsible for configuring its OpenTelemetry SDK, tracer provider and exporter.
See the OpenTelemetry integration guide for setup, exported attributes and limitations.
Examples
Practical examples are available in the examples directory, and as walkthroughs organized by problem in the documentation:
decorator_usage.py— beginner-friendly decorator APIbasic_resilience.py— retry, timeout and execution reportsfallback_chain.py— primary provider failure with fallback executioncircuit_breaker.py— circuit breaker protection with fallback behaviorvalidation.py— result validation with retry supportjson_schema_validation.py— JSON Schema response validation with retry recoverypydantic_validation.py— Pydantic response validation with fallback recoverystructured_events.py— operation lifecycle events with retry and validationidempotency.py— duplicate call coordination and result replayrate_limit.py— provider retry-after handling and maximum wait enforcementsqlite_event_store.py— durable structured events and basic history queriesopentelemetry_integration.py— structured reliability events exported to an active OpenTelemetry span
Every example runs without credentials or network access. If you run examples from a cloned repository, install RelPrim in editable mode first:
python -m pip install -e ".[dev]"
python examples/decorator_usage.py
Or run a single example without installing the package:
PYTHONPATH=src python examples/decorator_usage.py
Documentation
The full documentation, including scenario-based examples, is at bartrozycki.com/relprim.
Source-level guides in this repository:
- Getting started
- Advanced usage
- Idempotency
- Rate-limit handling
- SQLite event store
- OpenTelemetry integration
- JSON Schema validation
- Pydantic validation
Reference:
Stability
RelPrim follows semantic versioning. The public API, documented behavior, event identifiers and documented metadata keys only change incompatibly in a new major release. Underscore-prefixed names and undocumented internals are not part of that promise. See stability and versioning.
RelPrim supports Python 3.11–3.14.
Design principles
RelPrim is intentionally small and explicit.
Core principles:
- Reliability behavior should be visible in code.
- Failure modes should be explicit.
- Defaults should be safe for production use.
- Primitives should be composable, not magical.
- Observability should be built into the execution model.
- Async execution should respect cancellation and timeout semantics.
- The library should not hide side effects behind fake safety guarantees.
- External integrations should be wrapped, not replaced.
RelPrim does not try to become a workflow engine. It provides the reliability layer that can be used inside your application, worker, service or orchestration system.
What RelPrim is not
RelPrim is not:
- an AI provider SDK
- an HTTP client
- a workflow engine
- a task queue
- an observability backend
- a replacement for provider-native SDKs
- a replacement for Temporal, Celery or OpenTelemetry
It is a reliability layer for external operations.
Maintainer
Created and maintained by Bart Rozycki.
License
Apache License 2.0
Metadata
Release files for relprim 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| relprim-1.0.0.tar.gz | 115.2 kB | Without provenance | Details for relprim-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| relprim-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for relprim-1.0.0-py3-none-any.whl |
Total release size: 165.1 kB
Release files / relprim-1.0.0.tar.gz
| Download URL | relprim-1.0.0.tar.gz |
|---|---|
| Size | 115.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d9246876d1388f381f0afdfed0e7af4814a5a7735a7e57aba797e7a1617129f6
|
|
BLAKE2b-256 checksum How to use checksums |
8cc1fa809a773ab14ac3b23171a0d8367314bc098895d50d9c30acc89c4a7b56
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Release files / relprim-1.0.0-py3-none-any.whl
| Download URL | relprim-1.0.0-py3-none-any.whl |
|---|---|
| Size | 50.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
60c85ab582c6c4eb2c065dd136823267724e824b09c63d6e630c80bcfa2c1fe6
|
|
BLAKE2b-256 checksum How to use checksums |
8648594251051adc3d91205e9efd829e7ac2c8989aff2fd3f5deb050cfd58d7d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Security for relprim 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.