fsrest
fsrest provides reusable single-resource REST CRUD orchestration with action
names familiar to Django REST Framework users. It is framework-independent:
request and response objects are Pydantic models, while persistence is supplied
through a small repository protocol.
Designed and developed by Codex.
Install
pip install fsrest
Python 3.9+ and Pydantic 1.10/2.x are supported.
DRF-style actions
CrudViewSet follows DRF's standard action vocabulary:
listretrievecreateupdatepartial_updatedestroy
It is deliberately not an HTTP view and does not depend on Django. A FastAPI, Flask, Django, or other framework adapter can call these actions after request validation.
Bind a repository to CrudViewSet. Request schemas provide the conversion
methods needed to turn HTTP-facing data into repository fields.
from typing import Optional
from pydantic import BaseModel
from fsrest import CrudViewSet, PageRequest, PageResponse
class Item(BaseModel):
id: str
name: str
class Filters(BaseModel):
name: Optional[str] = None
class Ordering(BaseModel):
field: str = "id"
class CreateFields(BaseModel):
name: str
class UpdateFields(BaseModel):
name: Optional[str] = None
class PageData(BaseModel):
items: list[Item]
total: int
class ListQuery(PageRequest):
name: Optional[str] = None
def build_filters(self) -> Filters:
return Filters(name=self.name)
def build_ordering(self) -> Ordering:
return Ordering()
def build_response(self, *, page_data: PageData) -> PageResponse[Item]:
return PageResponse[Item](
items=page_data.items,
total=page_data.total,
page=self.page,
page_size=self.page_size,
)
class ItemRepository:
@classmethod
def list_page(
cls,
*,
filters: Filters,
ordering: Ordering,
page: int,
page_size: int,
) -> PageData:
...
@classmethod
def retrieve(cls, *, lookup_value: str) -> Optional[Item]:
...
@classmethod
def create(cls, *, fields: CreateFields) -> Item:
...
@classmethod
def update(
cls,
*,
lookup_value: str,
fields: UpdateFields,
) -> Optional[Item]:
...
@classmethod
def destroy(cls, *, lookup_value: str) -> bool:
...
class ItemViewSet(CrudViewSet):
repository = ItemRepository
Framework code can now use familiar action names:
page = ItemViewSet.list(query=query)
item = ItemViewSet.retrieve(query=lookup)
created = ItemViewSet.create(payload=create_payload)
updated = ItemViewSet.update(payload=update_payload)
patched = ItemViewSet.partial_update(payload=patch_payload)
result = ItemViewSet.destroy(payload=delete_payload)
Customizing behavior
Subclass a viewset and override only the smallest relevant hook. The public actions stay unchanged, so framework adapters do not need special cases.
class TenantItemViewSet(ItemViewSet):
not_found_message = "Item {lookup_value} does not exist"
@classmethod
def get_repository(cls) -> type[ItemRepository]:
# Select a repository at runtime, for example by tenant context.
return repository_for_current_tenant()
@classmethod
def get_filters(cls, *, query: ListQuery) -> Filters:
filters = super().get_filters(query=query)
return filters.model_copy(update={"tenant_id": current_tenant_id()})
@classmethod
def perform_create(cls, *, fields: CreateFields) -> Item:
item = super().perform_create(fields=fields)
publish_item_created(item)
return item
Available customization layers:
| Concern | Hook |
|---|---|
| Runtime persistence selection | get_repository |
| URL/request lookup extraction | get_lookup_value |
| Object loading | get_object |
| Filters and ordering | get_filters, get_ordering |
| Pagination execution | paginate |
| List response construction | build_list_response |
| Create/update field conversion | get_create_fields, get_update_fields |
| Persistence side effects | perform_create, perform_update, perform_destroy |
| Error construction and messages | get_exception, handle_not_found, handle_destroy_failure |
get_update_fields(payload, partial=...) receives whether the caller used
update or partial_update, so applications can implement PUT/PATCH semantics
without replacing either action.
For Pydantic 1 and 2 compatible PATCH field extraction, use model_to_dict:
from fsrest import model_to_dict
class ItemViewSet(CrudViewSet):
repository = ItemRepository
@classmethod
def get_update_fields(
cls,
*,
payload: UpdatePayload,
partial: bool,
) -> UpdateFields:
values = model_to_dict(payload, exclude_unset=partial)
values.pop("id", None)
return UpdateFields(**values)
Composing capabilities
Like DRF, fsrest exposes action mixins for building smaller viewsets:
from fsrest import (
CreateModelMixin,
GenericViewSet,
ListModelMixin,
)
class CreateListItemViewSet(
CreateModelMixin,
ListModelMixin,
GenericViewSet,
):
repository = ItemRepository
Available mixins are ListModelMixin, RetrieveModelMixin,
CreateModelMixin, UpdateModelMixin, and DestroyModelMixin.
For the common read-only case, use the precomposed viewset. If an adapter uses
SchemaSet metadata, only the schemas needed by that adapter have to be
provided; all fields default to None:
from fsrest import ReadOnlyViewSet, SchemaSet
class RetrieveQuery(BaseModel):
id: str
class PublicItemViewSet(ReadOnlyViewSet):
repository = ItemRepository
schema_set = SchemaSet(
item_schema=Item,
list_req_schema=ListQuery,
list_resp_schema=PageResponse,
list_filter_schema=Filters,
ordering_schema=Ordering,
page_data_schema=PageData,
get_req_schema=RetrieveQuery,
)
ReadOnlyViewSet exposes only list and retrieve; write actions are not
present. Therefore its SchemaSet does not need create_req_schema,
create_fields_schema, update_req_schema, update_fields_schema, or
delete_req_schema. CrudViewSet remains the precomposed full CRUD option.
The library raises RestApiError for missing records and failed deletes. To integrate with an application's existing exception middleware, subclass it and bind error_class:
class ApplicationApiError(RestApiError):
error_code = 400455
class ItemViewSet(CrudViewSet):
repository = ItemRepository
error_class = ApplicationApiError
Migrating from 0.1
The 0.1 API remains available for compatibility. New code should prefer these names:
| 0.1 API | 0.2 API |
|---|---|
RestCrudLogicBase |
CrudViewSet |
dao_rest_crud |
repository |
list_items |
list |
get_item |
retrieve |
create_item |
create |
update_item |
update |
delete_item |
destroy |
RestPageReqSchema |
PageRequest |
RestPageRespSchema |
PageResponse |
RestDeleteRespSchema |
DestroyResponse |
Repository method names used before 0.5 remain supported automatically:
| Before 0.5 | 0.5+ |
|---|---|
list_schema_page |
list_page |
get_schema_by_id |
retrieve |
create_schema |
create |
update_schema_by_id |
update |
delete_by_id |
destroy |
Development and publishing
From the pytools repository root, use the unified release script:
python make.py fsrest test
python make.py fsrest build
python make.py fsrest publish
publish uploads every artifact under fsrest/dist/ using the PyPI credentials
configured in ~/.pypirc. Before publishing a new release, update the version
in pyproject.toml, run tests, build fresh artifacts, and make sure dist/
contains only the version being released. PyPI rejects files for versions that
have already been published.
Metadata
Release files for fsrest 0.6.1
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 |
|---|---|---|---|---|
| fsrest-0.6.1.tar.gz | 35.1 kB | Without provenance | Details for fsrest-0.6.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fsrest-0.6.1-py3-none-any.whl | Python 3 | none | any | Details for fsrest-0.6.1-py3-none-any.whl |
Total release size: 46.0 kB
Release files / fsrest-0.6.1.tar.gz
| Download URL | fsrest-0.6.1.tar.gz |
|---|---|
| Size | 35.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
165d3e85d68d263784a78b991bf05c6f8c5a3e199873972b72b14fac6dced5dd
|
|
BLAKE2b-256 checksum How to use checksums |
a83c3c60d4b70a30be6f49c6142b29f3aa31f677526915183c48fc14485cbb8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / fsrest-0.6.1-py3-none-any.whl
| Download URL | fsrest-0.6.1-py3-none-any.whl |
|---|---|
| Size | 11.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96aea98eaa214aeb7cc7ebf4664337509fac9eb5d1d3abd0c7560b14b69eea81
|
|
BLAKE2b-256 checksum How to use checksums |
d357e98b185bec441619f7e6b6b163783fe45b2c4b58f4fa76ba4631141073f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Security for fsrest 0.6.1
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.