Service layer base class with domain error hierarchy
Project description
csrd.service
Service-layer boilerplate for the CSRD pattern (Controller → Service → Repository / Delegate).
What it provides
| Component | Purpose |
|---|---|
BaseService |
Lightweight base class with per-class logger and request-context accessors (current_user(), current_request_id()) |
ServiceError |
Base domain exception — keeps business logic free of HTTPException |
NotFoundError |
404 — resource doesn't exist |
ConflictError |
409 — duplicate, version mismatch, etc. |
ValidationError |
422 — business-rule validation (not schema validation) |
AuthorizationError |
403 — authenticated user lacks permission |
DownstreamError |
502 — a delegate call to another service failed |
service_exception_handler |
FastAPI handler that maps any ServiceError → structured APIErrorResponse JSON |
Consumer Feature Matrix (BaseService + ServiceError)
| Capability area | Common expectation | Actual support | How to use / notes |
|---|---|---|---|
| Request context access | Service can read current user and request id | ✅ Built-in | BaseService.current_user() and BaseService.current_request_id() |
| Service base class | Lightweight base for domain services | ✅ Built-in | Inherit from BaseService; inject repo/delegate dependencies in your constructor |
| Domain error taxonomy | Business errors map to consistent HTTP statuses | ✅ Built-in | Use NotFoundError (404), ConflictError (409), ValidationError (422), AuthorizationError (403), DownstreamError (502) |
| Structured API error responses | Service exceptions become API error payloads | ✅ Built-in (with registration) | Register service_exception_handler for ServiceError |
| Per-error status override | Override default status code per raise | ✅ Built-in | raise ValidationError(..., status_code=409) |
| Error metadata | Include detail/code fields for clients | ✅ Built-in | detail and code are preserved by the handler |
| Logging integration | Auto logging from service methods | ⚙️ Via composition | Mix in csrd.logging.LoggingMixin; not part of BaseService itself |
| Transport-agnostic domain logic | Avoid HTTPException in business layer |
✅ Built-in pattern | Raise ServiceError subclasses and keep HTTP concerns in handler layer |
| Dependency injection wiring | FastAPI DI helper exists in package | ⚙️ Pattern-based | Use your own factory + Depends; package documents the pattern |
| Transactions / unit-of-work | Automatic transactional boundaries | ❌ Not built-in | Handle in repository/DB layer |
| Idempotency utilities | Built-in idempotency keys/replay protection | ❌ Not built-in | Implement per endpoint/service policy |
| Policy engine / RBAC framework | Full authorization policy runtime | ❌ Not built-in | Use claim checks in service code and/or external policy layer |
Dependency tier
Tier 1 (standalone) csrd.models · csrd.lifespan · csrd.context
Tier 2 (uses Tier 1) csrd.delegate csrd.repository csrd.service
Tier 3 (uses Tier 1+2) csrd.versioning
csrd.service depends on csrd.context and csrd.models only. It does not import csrd.repository or csrd.delegate — your services accept those as constructor params.
Quick start
from csrd.service import BaseService, NotFoundError, DownstreamError
class OrderService(BaseService):
def __init__(self, repo: OrderRepository, payments: PaymentsDelegate):
super().__init__()
self._repo = repo
self._payments = payments
async def get_order(self, order_id: int) -> Order:
order = await self._repo.get_by_id(order_id)
if order is None:
raise NotFoundError("Order not found", detail=f"order_id={order_id}")
return order
async def place_order(self, cart: Cart) -> Order:
order = await self._repo.create(cart)
try:
await self._payments.charge(order)
except Exception as exc:
raise DownstreamError("Payment failed", cause=exc) from exc
return order
Register the exception handler
With csrd.versioning:
from csrd.service import ServiceError, service_exception_handler
from csrd.versioning import VersionedApiConfig
configure_versioned_api(
app,
version_mapping=VERSIONS,
config=VersionedApiConfig(
ex_handlers=[(ServiceError, service_exception_handler)],
),
)
Or on a plain FastAPI app:
app.add_exception_handler(ServiceError, service_exception_handler)
Wire into FastAPI DI
from functools import cache
from typing import Annotated
from fastapi import Depends
@cache
def order_service_factory(repo: OrderRepository, payments: PaymentsDelegate):
return OrderService(repo, payments)
OrderServiceDep = Annotated[OrderService, Depends(order_service_factory)]
# In your endpoint:
@router.get("/orders/{order_id}")
async def get_order(service: OrderServiceDep, order_id: int):
return await service.get_order(order_id)
Installation
uv add csrd-service # or: pip install csrd-service
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file csrd_service-0.3.65.tar.gz.
File metadata
- Download URL: csrd_service-0.3.65.tar.gz
- Upload date:
- Size: 8.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1feba09e79fbce975459b9f0b8b2e242ed4e78810df40d5dd4e988b045c8dbd3
|
|
| MD5 |
6795d3e1897fb90a4d6a6baa39525834
|
|
| BLAKE2b-256 |
ed5353fc89583d146b5396e9db28d7dfebf82deea97cab6d537327f21bd08bee
|
Provenance
The following attestation bundles were made for csrd_service-0.3.65.tar.gz:
Publisher:
release.yml on csrd-api/fastapi-common
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
csrd_service-0.3.65.tar.gz -
Subject digest:
1feba09e79fbce975459b9f0b8b2e242ed4e78810df40d5dd4e988b045c8dbd3 - Sigstore transparency entry: 1435885637
- Sigstore integration time:
-
Permalink:
csrd-api/fastapi-common@82e5251400c487f2a06849615b0baddadcf22721 -
Branch / Tag:
refs/tags/v0.3.65 - Owner: https://github.com/csrd-api
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@82e5251400c487f2a06849615b0baddadcf22721 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file csrd_service-0.3.65-py3-none-any.whl.
File metadata
- Download URL: csrd_service-0.3.65-py3-none-any.whl
- Upload date:
- Size: 7.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8ddbdf9bec81c3ae44803761906c6d10b8f9b47b144ee6c679dfd6701f2a766
|
|
| MD5 |
7161c56c07d364bb1d1ac9b7ca01e3d5
|
|
| BLAKE2b-256 |
8c3a0563c97dd0469618551e015ab686f06c813d9fc46962d50dc8b3a75ed91c
|
Provenance
The following attestation bundles were made for csrd_service-0.3.65-py3-none-any.whl:
Publisher:
release.yml on csrd-api/fastapi-common
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
csrd_service-0.3.65-py3-none-any.whl -
Subject digest:
d8ddbdf9bec81c3ae44803761906c6d10b8f9b47b144ee6c679dfd6701f2a766 - Sigstore transparency entry: 1435885668
- Sigstore integration time:
-
Permalink:
csrd-api/fastapi-common@82e5251400c487f2a06849615b0baddadcf22721 -
Branch / Tag:
refs/tags/v0.3.65 - Owner: https://github.com/csrd-api
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@82e5251400c487f2a06849615b0baddadcf22721 -
Trigger Event:
workflow_dispatch
-
Statement type: