Changelog#
FastAPI-Restly follows Semantic Versioning. While the
version is below 1.0.0, breaking changes may land in minor releases
(0.x → 0.y) and are always listed below under the release that ships them;
patch releases are fixes only. To opt into fixes without surprises, pin to a
minor version, for example fastapi-restly~={{ release }}.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased#
0.11.0 - 2026-10-09#
This release has many breaking changes. Before, we said to expect small breaking changes in deeper extension points, and this release changes more than that. If this affects your project: First of all, thank you for being one of the very first users. Secondly, we are working towards the best API we can make before freezing it in 1.0. That’s when the promise of stability comes. There will be more breaking changes every minor release until that time.
Added#
fr.NumberedPaginationandfr.NoPaginationhold a view’s pagination settings, including the model of the list response.A view can set its own response class with
schema_response. A custom route can usefr.schemas.derive_schema_responseandfr.schemas.derive_schema_list_responseas itsresponse_model.fr.exc.RestlyDuplicateSchemaNameWarningwarns when two classes in the OpenAPI spec have the same name.
Changed#
Names around lists and schemas follow the rules in CONTRIBUTING.md. A view that still uses an old name fails with an error that names the new one.
listing_param_schemais nowschema_list_params.fr.ListingResultis nowfr.ListResult, and itsquery_paramsis nowlist_params.fr.ResponseShape.LISTINGis nowfr.ResponseShape.LIST.to_listing_response(query_params, listing_result)and react-admin’sto_react_admin_listing_responseare nowto_list_response(list_result).to_response_schemais nowto_single_response.The
query_paramsargument and route parameter are nowlist_params.The view method
apply_query_paramsis nowapply_list_params.fr.query.apply_list_params(params, select_query, model, schema_cls)is nowfr.query.apply_list_params(query, list_params, model, schema_response).fr.query.create_list_params_schemais nowfr.query.derive_schema_list_params.fr.schemas.create_schema_from_model(model_cls, schema_name)is nowfr.schemas.derive_schema(model, name=...), withoutinclude_readonly_fieldsandinclude_relationships.
Restly names the classes it generates
<Resource><Role>, after the view’s schema without a finalSchema. ForUserSchemathey areUserResponse,UserCreate,UserUpdate,UserListResponseandUserListParams, so the types of generated clients change. A view without a schema getsUserSchemainstead ofUserRead.A response is an instance of the response class: the view’s schema without its
WriteOnlyfields. Compare responses withmodel_dump(), not with==. A response cannot be pickled.The list params and the relationships that a view loads follow the response class. A
to_single_responseoverride that reads a relationship that only aWriteOnlyfield names must load it inget_relationship_loader_options.paginationreplacespaginated,default_page_sizeandmax_page_size, also infr.query.derive_schema_list_params. Writepagination = Noneforpaginated = False, andpagination = fr.NumberedPagination(default_page_size=25, max_page_size=200)for the sizes.to_list_responseandget_many_endpointreturn the list response model, not a dict.React-admin views answer
400for arangewith more rows thanmax_page_size, which is 1000 by default.Dotted filters and sorts join through SQL aliases. Set
include_aliases=Trueonwith_loader_criteriarules so they apply to these joins.Item routes use
/{id:int}for integer keys and/{id:uuid}for UUID keys. An id that does not match answers404instead of422.Default route names are
get_many,get_one,create,update,deleteand react-admin’sput. Operation IDs areitems_list,items_getand so on, and the summary ofGET /{id}is “Get”. A view’sroute_optionscan set the old ones.Set custom session generators before you register routes that use
SessionDeporAsyncSessionDep.SQLAlchemy 2.0.45 or newer is required.
fr.ContextParamno longer has SQLAlchemy operator methods such asin_().Generated create and update schemas leave out a primary key that the server generates. Declare
schema_createto let clients send it.A view’s
dependenciesandresponsesadd up from the base class down, asprefixdoes, also from a mixin listed after the view base. A subclass can no longer remove a base dependency or response.The
fr.objectswrite helpers no longer takeschema_cls. A field that an explicitschema_createorschema_updatedeclares is written even when the response schema marks itReadOnly, so keep server-set fields, such as a tenant id, out of those schemas.A view class attribute with a
Query()or other request parameter marker raisesRestlyConfigurationError. Declare the parameter on the endpoint method instead.
Fixed#
Restly works with FastAPI 0.137 and later.
A field with a
default_factoryno longer makes/openapi.jsonanswer500.A list endpoint accepts the query keys that its dependencies read, and a custom list route can take its own query parameters. A hand-written
schema_list_paramstakes a field by its alias.A create saves the foreign key that the request sends, and works when a dataclass relationship has no default. An update can write a to-many relationship that the view does not load.
A view subclass keeps the schemas that its base declares.
The
{id}path parameter takes the model’s primary key type, so UUID and string keys work withoutid_type.Generated schemas take their fields from the SQLAlchemy mapper, so models with
from __future__ import annotations, SQLModel tables and plainColumnattributes get all their columns.A mapped class that does not subclass
DeclarativeBase, such as a SQLModel table, works as a view’smodeland with reference types. A SQLModel class works as a view’sschema.Configured session generators share one session per request with the app’s own dependencies.
Dotted filters and sorts work over a self-referential relationship.
Context members work as
mapped_column(default=Current.user_id)on models that are not dataclasses.React-admin views answer
400for a negative or reversedrange.Fixes for SQLAlchemy 2.1, Python 3.10, Python 3.14, mypy and pyright.
Security#
List endpoints no longer filter or sort on
WriteOnlyfields or on fields withField(exclude=True), which let a client read a hidden value back. A response withextra="allow"no longer sends a key with the name of aWriteOnlyfield.
0.10.0 - 2026-09-23#
Added#
fr.ContextNamespaceandfr.ContextParamsupport an application-definedCurrentcontext, with manual and FastAPI dependency binding.Reusable query predicates through
fr.where_clause, boolean composition, andfr.apply_clauses. Group them withfr.ClauseNamespace.Model default scopes filter view reads and reference checks. Views and individual reads can replace the scope or opt out with
fr.clauses.UNSCOPED.shared_write_action_commit()groups several write actions in one transaction. After-hooks run only after a successful commit.Retrieve, update, and delete handlers accept SQLAlchemy expressions to identify rows by natural or composite keys.
Custom view routes can use generated
query_params. Passwhere=tohandle_get_many()to narrow a listing without replacing its scope.restly new <name>generates a project with optional database and Alembic setup.fr.configure(health="/health")adds a liveness endpoint.fr.utils.CurrentSettingsMixinprovides a sharedSettings.currentinstance.
Changed#
Mapped[datetime]andTimestampsMixinnow useDateTime(timezone=True). Datetime filters without an offset use UTC for these columns.Migrate affected PostgreSQL columns, preserving the timezone of existing values. A bare type change can shift stored timestamps. For values stored as naive UTC:
ALTER TABLE example ALTER COLUMN created_at TYPE timestamptz USING created_at AT TIME ZONE 'UTC';
Use
mapped_column(DateTime())to keep naive wall-clock values.Rename
before_commitandafter_commitoverrides tobefore_action_commitandafter_action_commit.get_one()andget_many()overrides must accept and forwardscope=. IncludeColumnElement[bool]in aget_one()override’sidannotation.handle_*,make_new_object,update_object, andsave_objectonRestViewandAsyncRestViewcan no longer be overridden. Move custom logic to business methods,authorize, endpoint methods, or action hooks. Use SQLAlchemy column defaults for server-stamped fields.Engines created by
fr.configure()enforce SQLite foreign keys and share in-memory databases across threads. Invalid references return 409.React-admin listings reject unknown query parameters with 422. Allow custom keys with
extra_query_params.Pydantic 2.11.7 or newer is required.
Removed#
build_query. Move visibility rules todefault_scopeor the view’sscope, listing joins and ordering toapply_query_params, and eager loading toget_relationship_loader_options.RestView.delete_objectandAsyncRestView.delete_object. Overridedeletefor soft deletion. Thefr.objectsdeletion helpers remain available.
Fixed#
Query filters support recursive and frozen schemas without running cross-field validators. Field validators receive the correct field name.
Nested Pydantic values serialize into JSON columns. Excluded fields and other unsupported document shapes require an explicit
TypeDecorator.MustExistandRefExistshonor session-level SQLAlchemy visibility rules.save_objectno longer expires related objects during async writes.Stable listing order supports renamed and composite primary keys.
SaaS example fixes for tenant isolation, authorization, and organization deletion.
0.9.0 - 2026-08-13#
Added#
fr.testing.configure_tests()for schema setup and per-test database isolation.db_cleanup=modes (rollback,delete, andnone) with per-run--restly-db-cleanupandRESTLY_DB_CLEANUPoverrides.Pooled asyncpg support for applications configured with only an async database; no synchronous database configuration or test-only
NullPoolengine is required.AsyncRestlyTestClientand therestly_async_clientfixture.SQLAlchemy
AsyncAttrssupport throughawaitable_attrson Restly models.get_relationship_loader_options()as an eager-loading override.PostgreSQL CI coverage for dialect-specific behavior and database fixtures.
Public
EnvelopeandPaginatedEnveloperesponse models.
Changed#
List endpoints now paginate by default.
GET /thingschanges from an unbounded bare array to{"data": [...], "total_count": ..., "page": ..., "page_size": ..., "total_pages": ...}, capped atdefault_page_size(50). Existing paginated responses must renameitemstodataandtotaltototal_count.include_pagination_metadataandto_paginated_listing_response()are removed: setpaginated = Falsefor an unpaginated{"data": [...]}response with no pagination query parameters, overrideto_listing_response()to customize that envelope, or replaceget_many_endpointwith a matchingresponse_modelif the API must keep returning a bare array.default_page_sizemust now be an integer from 1 throughmax_page_size; the oldNonevalue is replaced bypaginated = False. Single-object responses are unchanged.After
configure_tests(), rollback isolation now applies to every test that uses the configured database, including client-only tests and code that callsfr.open_session()orfr.open_async_session()without requesting a public session fixture. Sessions join one test transaction through SQLAlchemy savepoints, socommit()androllback()behave as in production. A value added through a session fixture becomes visible to a request afterflush()orcommit(); the sessions do not share an identity map.Under rollback cleanup, an application configured with only an async database can no longer combine the synchronous
restly_clientwithrestly_async_sessionor other direct async database access. That would use one transaction from two event loops, so Restly raises a configuration error before database work begins and directs the test torestly_async_client. Under"delete"and"none", the same check applies to drivers such as asyncpg that bind pooled connections to one event loop.Under rollback cleanup, requesting
restly_sessionorrestly_async_sessionwith only a custom session generator now raises instead of skipping. Configure the corresponding database URL, engine, or sessionmaker as well so Restly can build the isolated session.
Fixed#
restly_clientnow runs application lifespan startup and shutdown once per test. A view registered throughrestly_client.appafter the fixture has constructed the client also keeps working, including Restly’s integrity-error handling.PostgreSQL constraint errors now produce clean details with psycopg 3.
Write responses now eager-load schema relationships, avoiding
MissingGreenleterrors and sync N+1 queries.Rollback isolation now routes applications that configure a custom session generator alongside a matching sessionmaker through the isolated factory, instead of letting request writes escape the test transaction.
restly_project_rootnow resolves from the requesting test file, including in monorepos.restly_async_sessionno longer errors when a synchronous sessionmaker is also configured. Under rollback cleanup, it uses the same pinned connection asrestly_session, so committed writes are visible between them within a test.Engine and schema helpers now work inside session fixtures.
Removed#
activate_savepoint_only_mode()anddeactivate_savepoint_only_mode()are removed. They isolated each session separately rather than the whole test, so a write from one request was not reliably visible to the next. Callfr.testing.configure_tests()once inconftest.pyinstead; it gives the client, session fixtures, and directly opened sessions one transaction for the whole test.
0.8.0 - 2026-07-22#
Added#
fr.MustExist[int, Post]— an existence-checked scalar foreign key.post_id: fr.MustExist[int, Post]validates that the referenced row exists (a clean 404 on a miss, batched — no N+1) while the field stays a plain scalar everywhere: on the wire, in the column, in hooks (data.post_idis the id itself, not anIDRef/IDSchemawrapper), and to a type checker (data.post_idisint). The primary-key type comes first (fr.MustExist[UUID, Account]for a UUID key); when the column has a singleForeignKey, you can drop the model and let Restly infer it —fr.MustExist[int]. ReserveIDRef/IDSchemafor relationship-named fields.The opt-in
fr.configure(warn_on_misuse=True)lint now flags thepost_id: fr.IDRef[Post]mistake — anIDRef/IDSchemareference typed on a scalar foreign-key column, wheredata.post_idbecomes a wrapper instead of the plain id — steering tofr.MustExist[int, Post].IDRef/IDSchemaare now documented as relationship-named types, with*_idcolumns pointed atMustExist.
Changed#
The tier vocabulary in docstrings and error messages now matches the docs: endpoint method (was “route shell”), handler (was “request handler”), and business method (was “business verb”). The misuse warning (
RestlyMisuseWarning) and the bare-verb route-nameTypeErroruse the new terms; match on"endpoint method"/"business method"if you asserted on those messages.View.tagsis now typedClassVar[Iterable[str | Enum] | None]instead ofClassVar[Any], matching what FastAPI accepts for router tags. Runtime behaviour is unchanged; type checkers now flag a wrongly-typedtags.The
[testing]extra now installshttpx2alongsidehttpx. Newer StarletteTestClientprefershttpx2and emits aStarletteDeprecationWarningwhen onlyhttpxis present; shipping both keepsRestlyTestClienton the non-deprecated path without dropping support for the older Starlette versions in our range, which still importhttpxdirectly.RestlyTestClientnow prefershttpx2(falling back tohttpx), matching Starlette’s ownTestClient. The “install the[testing]extra” hint that Restly raises when the test client is missing now also fires under newer Starlette, which signals the absence ashttpx2(and via aRuntimeError) rather than a missinghttpx.
Removed#
The
orjsondependency and the orjson serializer on URL-created engines.JSONcolumns now use SQLAlchemy’s default encoder (the standard libraryjson) on every engine: naive datetimes are no longer silently stamped as UTC, and datetime/UUID values or keys now raiseTypeErrorat write time instead of being coerced to strings. To keep the old behavior, build your own engine withjson_serializer=/json_deserializer=and pass it tofr.configure().
Fixed#
Filtering on a dotted path deeper than one relationship hop (
?city.country.code=NL) no longer fails with a 500. Deep paths were advertised and resolved, but the filter clause collected its joins into an unordered set, so the second hop could be joined before the first — an implicit cartesian product that the database rejects as an ambiguous join. Filter joins now apply in path order, as sorting always did. Two shapes remain unsupported and are now documented as such: paths through a self-referential relationship, and two filter paths that reach the same table — those need per-path join aliasing.List views no longer advertise filter parameters that can never execute for collection-typed columns (
JSON/ARRAY, i.e. fields typeddict,list[...],Sequence, and similar). A query-string value cannot coerce into a collection, soeq/__in/__ne— and, for parametrized generics such aslist[str], the range family — answered 400 on every request; those parameters are now omitted from the generated schema and OpenAPI, and only__isnull, which works, is kept. On generated endpoints, requests using the removed parameters now fail validation as unknown parameters (422) instead of passing validation and failing at execution; callers passing rawQueryParamstoapply_list_paramsstill get the 400.pydantic.Json[...]fields keep their filters: their validation parses the query string into the collection, so they execute.Registering the same view class twice on the same app or router (
fr.include_view(app, V); fr.include_view(app, V)— a double import, or the decorator form combined with an explicit call) no longer mounts its routes twice. The duplicate call is now a no-op: each parent tracks the view classes already mounted on it, and an app and its.routerattribute count as one parent. The opt-infr.configure(warn_on_misuse=True)lint flags the duplicate call with aRestlyMisuseWarning. Registering on different parents (a public and an admin app,/v1and/v2sub-apps) still mounts on each, as before.fr.IDRef[T]/fr.IDSchema[T]foreign-key fields now work under any column name, not only fields ending in_id. A field likepost_fk: fr.IDRef[Post]backed by a non-_idFK column was silently misrouted — the resolved ORM object was assigned into the integer FK column and the request failed at flush (sqlalchemy.exc.ProgrammingError) instead of at validation. Reference routing (the create plan, the in-place update, and the both-supplied FK/relationship consistency check) now decides column vs. relationship and derives the partner attribute from the SQLAlchemy mapper rather than from the field name, so any FK column name resolves correctly.Creating with a relationship exposed as a reference field (e.g.
invoice: fr.IDRef[Invoice]) no longer fails when the model’s local FK column is a required constructor argument (noinit=Falseand no default) and a reference to an existing row is supplied. The resolved row’s id is now passed at construction instead of being assigned afterward, so the dataclass__init__no longer raisesTypeError: ... missing 1 required keyword-only argument. Declaring the FK columninit=Falseis still supported but no longer required.A null reference — an explicit
post=Nonefor apost: fr.IDRef[Post] | Nonefield, or such a field omitted and defaulting toNone— no longer raisesTypeError: __init__() missing 1 required keyword-only argument: 'post_id'when the model’s local FK column is a required constructor argument (noinit=Falseand no default). A null reference now takes a dedicated plan path that writes the field’s own slot and passes a partner kwarg (asNULL) at construction when the dataclass requires it — in either direction (FK column or relationship) and nothing more, so an unset sibling reference field (schemas may declare both names of an FK/relationship pair as reference fields) never clobbers the side the client supplied. A null reference to a nullable FK creates the row with a NULL FK; to a non-nullable FK it now fails at flush as a regularIntegrityError(the standard 409 path) instead of the 500TypeError.fr.IDRef[T]now serializes through the type itself under plain Pydanticfrom_attributes, so a reference field validated outside a Restly route — a nested model, a custom endpoint, orresponse_model=on a raw schema — no longer crashes with a crypticint_typeerror when the value is the related ORM row. Previously only the view layer’sto_response_schemahandled this, by pre-extracting the scalar id; that redundant special-casing is removed and bothIDRef[T]andIDSchema[T]self-serialize from a related row or a raw scalar id along a single path. Response output is unchanged.
0.7.0 - 2026-06-11#
Added#
The generated route shells (
get_many_endpoint,create_endpoint, …) now carry one-line override-redirect docstrings, sohelp(RestView), source readers, and coding agents see which tier to override (<verb>for domain logic,handle_<verb>for orchestration,to_responsefor shape). The docstrings are stripped from generated routes at registration so framework guidance never appears as OpenAPI operation descriptions in your API; endpoints you define or override yourself keep FastAPI’s normal docstring behavior.Scalar
fr.IDRef[T]foreign-key fields are now filterable on list endpoints by their own public name. Previouslypost_id: fr.IDRef[Post]— the FK form the tutorial teaches — generated no filter parameter at all, soGET /comments/?post_id=1returned a 422 that looked like client error; the only filterable form was a plainpost_id: int. AnIDRefid is treated as opaque, so it gets equality,__in,__ne, and__isnull(uniform across int/UUID/string primary keys) but not the range or substring operator families.Python 3.14 is now officially supported and tested. It was previously in the CI matrix as an experimental (allowed-to-fail) target while
orjsonlacked a 3.14 wheel; that wheel now ships, the full test suite passes on 3.14, and the job gates CI like every other supported version.fr.db.create_all(Base)/fr.db.async_create_all(Base)— dev/demo helpers that create every table for a declarative base (or aMetaData) on the engine configured viafr.configure(), replacing theengine = fr.db.get_async_engine(); async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)boilerplate in quickstarts and test setup. Use Alembic migrations in production.Opt-in registration-time misuse warnings:
fr.configure(warn_on_misuse=True)makesinclude_viewlint each registered view class and emitfr.exc.RestlyMisuseWarningfor the three dominant misuse patterns — overriding a route shell (<verb>_endpoint) where a business-verb override was meant, callingsession.commit()directly in a view method, and hand-rolling a CRUD route set on a bareViewinstead of subclassingRestView/AsyncRestView. Each message names the idiomatic fix. Off by default; intended for development, project templates, and CI.
Changed#
The
RestlyUncommittedChangesWarningmessage now leads with the fix (bracket the mutation withwrite_action(...)or reuse ahandle_<verb>) and offers only the per-route suppression (session.info["_fr_suppress_uncommitted"] = True) for intentional dry runs. It no longer advertises the globalwarn_on_uncommitted=Falseopt-out, which readers took as a fix for the warning instead of committing their changes.Breaking — top-level
fr.*namespace curated. Errors, HTTP exceptions, and the uncommitted-changes warning moved tofr.exc(fr.exc.NotFound,fr.exc.RestlyError, …) — theexceptionsmodule is renamedexc, mirroringsqlalchemy.exc. Advanced helpers moved to their layer submodules: schema↔ORM helpers tofr.objects.*(make_new_object/save_object/snapshot/ … and theasync_*variants), list query helpers tofr.query.*(create_list_params_schema,apply_list_params), engine accessors tofr.db.*(get_engine,get_async_engine), andIDMixintofr.models.*. The route decorators (@fr.get/@fr.post/ … /@fr.route), views, registration, schemas, model bases,configure, the session helpers/dependencies, and the view support types (Action/ViewRoute/ResponseShape/ListingResult) stay top-level. Migration: e.g.fr.NotFound→fr.exc.NotFound,fr.make_new_object→fr.objects.make_new_object,fr.get_async_engine→fr.db.get_async_engine.
Fixed#
fr.open_session()/fr.open_async_session()now resolve the same session source asSessionDep/AsyncSessionDep: a custom session generator passed tofr.configure(session_generator=...)/sync_session_generator=...takes precedence over the built-in factory. Previously the context managers always used the built-in factory, so a generator-only configuration worked inside request handlers but raisedRestlyConfigurationErroroff-HTTP (in scripts, background jobs, or a custom dependency wrappingopen_*session()). The two session entry points are now consistent.
0.6.1 - 2026-06-02#
Changed#
Reference resolution (
IDRef/IDSchema) no longer mutates the validated request model in place. The resolver now returns a{field: resolved}mapping that the write path consumes, so the request model keeps its wire shape (its reference fields stayIDRef[T]values rather than being overwritten with ORM rows). Behavior of create/update is unchanged; the internal helpersbuild_create_plan/apply_update_to_object/validate_resolved_reference_consistencygained aresolvedargument.The
standardextra is now runtime-only and mirrorsfastapi[standard]. It no longer pulls the test toolchain (pytest,pytest-asyncio,pytest-cov,httpx) or bundles theaiosqlitedriver, so installingfastapi-restly[standard]for production no longer drags pytest into the image. Test tooling stays in thetestingextra; the database driver is now an explicit choice (Restly remains driver-agnostic). Migration: if you relied on[standard]for test dependencies, switch to[testing]; if you ran on SQLite, addaiosqliteto your dependencies directly.The
testingextra now includes only the third-party packages Restly’s shipped test helpers import —pytest,pytest-asyncio, andhttpx(forRestlyTestClientand therestly_*fixtures).pytest-covis no longer pulled in; add it yourself if you want coverage reports.Removed the
docsextra from the published extras. It only installed the toolchain for building Restly’s own documentation site (a maintainer concern); those dependencies remain in thedevdependency group. The published extras are nowstandardandtesting.
Fixed#
RestlyUncommittedChangesWarningno longer false-positives on every write run under the savepoint test fixtures: the patchedcommitclears the pending-changes flag (mimicking the realafter_commit), while a genuinely forgotten commit still warns.An
IDReflist field that references the same id more than once no longer raises a confusingId not found: set()404 when the referenced rows all exist; a genuinely missing id is now named in the error.An
IDReflist field now resolves in the client-sent order instead of silently reordering to the database’s primary-key order (duplicate ids are collapsed, first occurrence wins).A
ReadOnlyorWriteOnlymarker nested inside a field’s type instead of wrapping it (such asOptional[WriteOnly[str]],WriteOnly[str] | None, orlist[WriteOnly[str]]) is now rejected with aRestlyConfigurationErrorinstead of silently no-op’ing. Nested there the marker has no effect — aWriteOnlyfield would leak into responses and aReadOnlyfield would stay writable — so the framework now raises when the schema is defined (and again at view registration for schemas that do not derive fromBaseSchema), pointing to the safeMarker[Optional[T]]form.A list view no longer advertises filter query parameters for fields that are not filterable columns — a to-many relationship (
books: list[BookRef]) or a reference field that does not resolve to a column. These appeared in OpenAPI but always returned 400. Filter-param generation now validates each field against the model with the same column-resolution predicate the request path uses, so non-column fields no longer get filter params; to-one dotted traversal is unchanged. (create_list_params_schemanow takes the queriedmodelas a required argument.)
0.6.0 - 2026-06-01#
Reworks the class-based view API around a three-tier “handle” design. This is a breaking change; views written for 0.5.x need updating.
Changed (breaking)#
Each CRUD verb now has three tiers: route shell (
*_endpoint), request handler (handle_*), and domain verb (get_many,get_one,create,update,delete). This replaces thelisting/get/create/update/deleteendpoints and the singleperform_*tier.The framework owns commits.
handle_<verb>andwrite_actionrunbefore_commit→ commit →after_commit; request-session dependencies no longer commit on response. Custom write routes should reusehandle_<verb>or bracket mutations withself.write_action(...). Manualsession.commit()is only for shapes the bracket does not model, such as batch commits.commit_session_on_responseis removed; custom session generators construct and clean up sessions but do not own commits.Renamed:
creation_schema/update_schema→schema_create/schema_update;build_from_schema/apply_schema→make_new_object/update_object;count_listing→count. Response shaping goes through a singleto_response(obj_or_list, shape=ResponseShape.SINGLE)method.ViewRoute.LIST/GET→ViewRoute.GET_MANY/GET_ONE.
Added#
authorize(action, obj, data)override — an empty override by default; raisefr.Forbidden/fr.NotFoundto gate a verb (row visibility goes inbuild_query).before_commit/after_committransaction hooks and asnapshot()helper for old-vs-new comparison.Typed request-time exceptions
NotFound,Forbidden,Conflict, andBadQueryParam, subclassingfastapi.HTTPException(so a singleapp.add_exception_handler(fr.NotFound, ...)can reshape them).Top-level
make_new_object/update_object/save_object/delete_object/snapshothelpers (and theirasync_*variants where applicable) for use outside a view.write_action(action, *, obj, data)— a context manager for custom write actions. It shares the CRUD authorize + commit bracket. Create-shaped actions that omitobj=must assign the yielded handle’s.objbefore exit.RestlyUncommittedChangesWarning(default on;warn_on_uncommitted=Falseto disable) when a request finishes with uncommitted changes — the tell of a write route that forgot to commit.
Fixed#
A
@routemethod named like a bare verb (create/update/delete/get_one/get_many) is now rejected at registration: it shadowed the verb and collided with its*_endpointroute shell.Registering a View subclass alongside its parent on the same app no longer duplicates the child’s routes.
React Admin list/count/update paths now use the same
build_query,count, authorization, and commit lifecycle as standard REST views; filter/sort resolution is limited to public schema fields.A paginated list sorted on a non-unique column now appends the primary key as a final
ORDER BYtiebreaker, so rows are no longer skipped or repeated across pages. Applies to both the standard and React Admin sort paths.A
build_querythat joins a to-many relationship no longer fans out:get_manyde-duplicates entities and the list total counts distinct rows, so the page andtotal_countagree. A no-op for queries without such a join.A
WriteOnlyfield no longer leaks into a response, including from a nested response schema.WriteOnlyfields are now excluded from serialization at the field level (exclude=Trueon the marker), so they are stripped recursively on the wire and dropped from the OpenAPI response schema, while staying required, documented request inputs. PreferWriteOnly[Optional[T]]overOptional[WriteOnly[T]]— aWriteOnlymarker buried only inside a union is not excluded.
0.5.1 - 2026-05-11#
Fixed#
Fixed
fr.include_view(...)registration onfastapi.APIRouterparents.
0.5.0 - 2026-05-06#
First public beta release.
Added#
Class-based CRUD views for async and sync SQLAlchemy sessions with generated list, get, create, update, and delete routes.
React Admin compatible
AsyncReactAdminViewandReactAdminViewvariants for thera-data-simple-restwire contract.Generated schema support for read, create, and update payloads, including
ReadOnly,WriteOnly,IDSchema,IDRef, and timestamp schema helpers.Standard list query support for filtering, sorting, pagination, relation aliases, and pagination metadata.
Public
RestlyErrorandRestlyConfigurationErrorexception hierarchy.Testing utilities through
RestlyTestClient, savepoint-only mode helpers, and thefastapi_restly.pytest_fixturespytest plugin.
Changed#
Consolidated framework setup on
fr.configure(...), including async/sync engine configuration and response-session commit policy.Renamed built-in route methods to resource-oriented names:
list,get,create,update, anddelete.Renamed business-logic hooks to
perform_list,perform_get,perform_create,perform_update, andperform_delete.Standardized schema component names on
ModelRead,ModelCreate, andModelUpdate.Made
sortthe standard list ordering parameter.Split
__containsand__icontainsso case-sensitive and case-insensitive matching have distinct public operators.Exposed savepoint-only testing helpers through
fastapi_restly.testinginstead of the top-level package namespace.Renamed
build_listing_querytobuild_queryand broadened its role:perform_getnow also routes through this hook, so a single override filters listing, the pagination total, and single-row fetches. A row hidden from listing returns 404 fromGET /{id}too, andperform_update/perform_deleteinherit the visibility check viaperform_get.perform_getnow issuesSELECT ... WHERE pk = ?instead ofsession.get(...). Single-column primary-key behavior is unchanged. Composite-primary-key subclasses must overrideperform_get.Advanced schema-to-object helpers now live in
fastapi_restly.objects:build_from_schema,apply_schema,save_object, anddelete_object, with async equivalents. The view methods use the same names for the mapping hooks and keep persistence at thesave_object/delete_objectboundary.
Removed#
Removed the pre-stable
query=argument fromperform_listing. Overridebuild_query()for SQL-level base query changes so listing, pagination totals, and single-row fetches stay aligned.Removed pre-release route and hook names such as
index,get,post,patch,delete,handle_list, andhandle_getfrom the public view API.Removed unsupported
get_one_or_createhelpers before the first stable release.Removed internal model helpers such as
TableNameMixin,underscore, andutc_nowfrom the public API surface.Removed duplicate pytest fixture exports from
fastapi_restly.testing; the pytest plugin path isfastapi_restly.pytest_fixtures.