Changelog¶
All notable changes to drf-restflow will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.0.0] - 2026-07-20¶
Changed¶
- BREAKING:
cache_ifandcache_unlessnow receive(result, *args, **kwargs)instead of just the result. The predicates are called with the function's return value followed by the call arguments, matching the new callablettl. A predicate written for an earlier release that accepts only the result must add*args, **kwargs. The same applies tocache_response, where the first argument is the rendered response.
# before
@cache_result(UserKey, ttl=300, cache_if=lambda result: result is not None)
def get_profile(user_id): ...
# after
@cache_result(
UserKey, ttl=300,
cache_if=lambda result, *args, **kwargs: result is not None,
)
def get_profile(user_id): ...
Added¶
-
restflow now ships a
py.typedmarker and a fully annotated public API. The package type-checks clean under pyright in standard mode withdjango-stubsanddjangorestframework-stubs, and atypecheckjob gates it in CI. Serializers, views, filters, permissions, caching, throttling, pagination, and the test client all carry real types instead ofAny. Because the package was previously untyped, a project running mypy or pyright in a strict configuration may surface new type errors in its own code where restflow symbols resolved toAnybefore. -
ttloncache_resultandcache_responseaccepts a callable that computes the time-to-live per call. It runs on a cache miss, right before the write, and receives(result, *args, **kwargs). Return an int number of seconds, orNoneto cache without expiry. On an async target the callable may beasync def.
@cache_result(
UserKey,
ttl=lambda result, *args, **kwargs: 60 if not result else 3600,
)
def get_report(brand_id, month):
return build_report(brand_id, month)
- A decorated function exposes
.func, the undecorated target typed with its original signature. Calling it runs the function directly and skips the cache, so it doubles as an escape hatch and as the function object to pass around or introspect. The stdlib__wrapped__attribute thatfunctools.wrapsalready sets is now declared with the same type.
@cache_result(UserKey, ttl=300)
def get_profile(user_id: int) -> Profile: ...
get_profile.func(1) # uncached call, typed (user_id: int) -> Profile
filterset_schema_methodscontrols which HTTP methods expose a view's filterset parameters in the OpenAPI schema.RestflowAutoSchemainjects filter parameters for every method of a view that declaresfilterset_class, includingPOST,PUT, andPATCH. Setfilterset_schema_methodson the view to limit them to specific methods (case-insensitive), or setSCHEMA.FILTER_METHODSinRESTFLOW_SETTINGSfor a project-wide default. A per-view attribute overrides the setting, andNonemeans every method. The default is unchanged, so existing schemas keep filter parameters on every method.
class ProductView(AsyncModelViewSet):
filterset_class = ProductFilterSet
filterset_schema_methods = ["GET"]
-
Instantiating a
SerializerorModelSerializersubclass now reports the concrete class to type checkers, andmany=TruereportsListSerializer, instead of theListSerializer | Anyunion inherited from DRF. IDE go-to-definition and autocomplete resolve to the subclass and its fields. Runtime behavior is unchanged, this only sharpens the static types. -
CursorPagination.apaginate_querysetnow paginates natively on the event loop with async ORM iteration instead of running DRF's syncpaginate_querysetin a thread viasync_to_async. It mirrors DRF's cursor logic and only the row fetch changed, so results are identical. ThePageNumberPagination,LimitOffsetPagination, andFastPageNumberPaginationasync paths were already native. A custom paginator that does not implementapaginate_querysetstill falls back tosync_to_asyncthroughBasePagination.
[1.4.1] - 2026-07-04¶
Added¶
cache_resultnow reports the wrapped callable's return type to type checkers. The decorator picksSyncCachedWrapperfor a plain function andAsyncCachedWrapperfor a coroutine function, so calling a cached sync function is typed as its return value and calling a cached async function is typed as a coroutine to await. Both classes are re-exported fromrestflow.caching. Runtime behavior is unchanged, this only sharpens the static types.
@cache_result(UserKey, ttl=300)
def get_profile(user_id: int) -> Profile: ...
@cache_result(UserKey, ttl=300)
async def aget_profile(user_id: int) -> Profile: ...
reveal_type(get_profile(1)) # Profile
reveal_type(aget_profile(1)) # Coroutine[Any, Any, Profile]
[1.4.0] - 2026-06-20¶
Added¶
KeyConstructor.wipe()invalidates the cache for every function that shares a constructor in one call.delete_by_prefix()andinvalidate_all()act on a single wrapped function, while the constructor keeps a list of the functions decorated with it and fans the same invalidation out across all of them. Arguments bind to each function's signature like a normal call, sowipe(user_id=42)drops that partition everywhere and a barewipe()clears every partition. Useawipe()for async functions. A function joins the set when its@cache_resultor@cache_responsedecorator runs at import time, and the call needs a redis-compatible backend that implementsdelete_pattern.
class UserDataKey(KeyConstructor):
user = ArgsKeyField("user_id", partition=True)
class Meta:
namespace = "user_data"
@cache_result(UserDataKey, ttl=300)
def get_profile(user_id): ...
@cache_result(UserDataKey, ttl=300)
def get_settings(user_id): ...
UserDataKey.wipe(user_id=42) # drops user 42 on both functions
UserDataKey.wipe() # drops every entry both functions wrote
KeyConstructor.delete_previous_versions()removes cache entries left behind by older versions of a constructor. BumpingMeta.versionmakes old entries unreachable but leaves the bytes in the backend until they expire. When the version is n above 1, this deletes versions 1 through n-1 across the namespace withcache.delete_pattern, leaving the live version n in place. A version of 1 is a no-op. The delete is scoped by namespace, so a constructor without one raises, and the backend must implementdelete_pattern. Useadelete_previous_versions()in async contexts.
Fixes¶
delete_cache(),delete_by_prefix(), andinvalidate_all()now delete at the constructor's version. Reads and writes already passedversion=fromMeta.version, but the delete paths did not, so on a constructor with a non-default version the deletes targeted version 1 and never matched the stored keys. The same version is now applied on every delete, including the async variants.
[1.3.0] - 2026-06-20¶
Added¶
NotRequired[T]annotation marker for serializer fields. It setsrequired=Falsewithout touchingallow_null, so a key may be left out of the input but must not be null when present. This complementsOptional[T]/T | None, which mark a field both optional and nullable. The marker is re-exported fromrestflow.serializers, composes with the other forms (NotRequired[T | None]is optional and nullable), and an explicitField(required=True)still takes precedence.
from restflow.serializers import NotRequired, Serializer
class SignupSer(Serializer):
email: str
nickname: NotRequired[str] # optional key, not nullable
referral: NotRequired[str | None] # optional key and nullable
- Per-lookup help text for filter lookup variants. A lookup alias can map to a dict carrying the ORM
lookupand an optionalhelp_text, so each generated variant can describe itself in the OpenAPI schema.
class ProductFilter(FilterSet):
price = IntegerField(
help_text="Product price",
lookups={
"min": {"lookup": "gte", "help_text": "Minimum price"},
"max": {"lookup": "lte"},
},
)
Each variant resolves its description in three steps. An explicit per-lookup help_text is used as is. Otherwise, when the parent field has a help_text, the variant gets "parent help (Bound Label)", for example "Product price (Inclusive Upper Bound)". Otherwise it falls back to an auto-generated verb hint. Negated (!) variants reuse the same description as "exclude where ...".
Breaking¶
- Dropped Python 3.10 support. The minimum supported version is now Python 3.11.
[1.2.0] - 2026-06-18¶
Fixes¶
- Filter fields no longer invent values for parameters the client never sent.
BooleanFieldandMultipleChoiceField(andOrderField) returnedFalseand[]for an absent key becauserequest.query_paramsis aQueryDictand DRF treats it as HTML form input. A missing parameter now skips the field, so a bare list request no longer applies afield=Falseorfield__in=[]filter that drops rows it should keep. delete_by_prefix()now clears keys built from a partition-onlyKeyConstructor.generate_keyhad stripped the trailing separator from the stored key while the delete pattern kept it, so the prefix never matched. The separator is preserved on both sides, and a value likeuser:42still does not matchuser:420.
Docs¶
- Note that
delete_by_prefix(),delete_cache(), andrefresh()bind arguments to the wrapped function's signature. Pass partition values by keyword so a positional value cannot bind to the wrong parameter.
[1.1.0] - 2026-05-26¶
Added¶
Caching¶
@cache_responsedecorator for whole-view and@api_viewHTTP caching. Stores the rendered response triple (content, status code, headers) and rebuilds a plainHttpResponseon a hit.set_cache_headers=Falseflag on@cache_response. When set toTrue, the wrapper attaches theX-Cached-at,X-Cache-reset-at, andX-Cache-statusheaders to every returned response so clients and monitoring can tell hits from misses without a separate metadata lookup.ResponseCacheKeyConstructordefault key constructor for@cache_response. Hashes the full query string and captures the view method's URL kwargs as the partition.ViewKwargsKeyFieldcache-key field that captures a view method's URL kwargs while skippingself,cls, andrequest.
Responses¶
restflow.responses.Responsewith anarender()method. Renders content and awaits any coroutine-function post-render callbacks on the live event loop. Async views and@cache_responseon async methods use this path to keep rendering on the loop instead of bouncing throughasync_to_sync.
Fixes¶
set_response_cache_headeremitted theX-Cache-statusheader as"CacheStatus.MISS"instead of"MISS"on Python 3.11+ becausestr(enum)now includes the class name. The helper now writes the enum'svalueso the header matches the documented vocabulary (HIT,MISS,STALE,BYPASS,REFRESH).ArgsKeyField.get_key_payloadnow usesinspect.Signature.bind_partialinstead ofbind, so invalidation handlers that supply only the fields named infield_mapping(for example, a view method whose signature includesselfandrequest) no longer raiseTypeError: missing a required argumentand abort the rule.
[1.0.2] - 2026-05-26¶
Fixes¶
- Fix circular import on Django startup when
restflow.authentication.JWTAuthenticationis listed inREST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]. The package no longer re-exports view classes at import time, breaking the cycle betweenrestflow.authentication,restflow.views, andrest_framework.viewsduring DRF settings resolution.
Breaking¶
TokenObtainView,TokenRefreshView, andTokenBlacklistVieware no longer re-exported fromrestflow.authentication. Import them fromrestflow.authentication.viewsinstead.
# Before
from restflow.authentication import TokenObtainView, TokenRefreshView, TokenBlacklistView
# After
from restflow.authentication.views import TokenObtainView, TokenRefreshView, TokenBlacklistView
[1.0.1] - 2026-05-25¶
Fixes¶
- Fix Response caching bug
[1.0.0] - 2026-05-19¶
Added¶
Serializers¶
Serializer,ModelSerializer, andHyperlinkedModelSerializerwith annotation-driven field declaration using Python type hintsFieldsentinel for layering DRF kwargs on top of annotated fields- Full async surface:
ais_valid,arun_validation,ato_internal_value,asave,acreate,aupdate,ato_representation ModelSerializerships default asyncacreateandaupdatethat mirror DRF's sync logic using the async ORMInlineSerializerfactory for building serializer classes at runtime without a dedicated class definitionValidatedDatadict subclass with attribute access andto_json()helperEmail,IPAddress, andBlankableStringtype aliases;SerializerFieldMapfor custom type-to-field mappingsDecimalFieldsubclass with sensible default precision (max_digits=20,decimal_places=6)
Views¶
APIViewwith the helper surface (get_serializer,validated_serializer,serialized_response,paginated_response) on top of DRF's sync viewAsyncAPIViewwith a fully async dispatch loop and async twins for every helper- Async generic views:
AsyncListAPIView,AsyncCreateAPIView,AsyncRetrieveAPIView,AsyncUpdateAPIView,AsyncDestroyAPIView, and all composite combinations - Async model mixins:
AsyncCreateModelMixin,AsyncListModelMixin,AsyncRetrieveModelMixin,AsyncUpdateModelMixin,AsyncDestroyModelMixin AsyncViewSet,AsyncGenericViewSet,AsyncReadOnlyModelViewSet,AsyncModelViewSetActionConfigdataclass for per-action serializer, permission, throttle, parser, renderer, pagination, and queryset overridesrequest_serializer_class/response_serializer_classsplit on viewsets andAPIViewPostFetchhelper for attaching related rows to paginated lists outside ofprefetch_related
Authentication¶
- Async-aware
BaseAuthenticationwithaauthenticateon every built-in DRF class:BasicAuthentication,SessionAuthentication,TokenAuthentication,RemoteUserAuthentication - Built-in JWT authentication (
JWTAuthentication) with access and refresh tokens, blacklist support via pluggable backends, refresh token rotation, and configurable claims - Pre-built JWT views:
ObtainTokenView,RefreshTokenView,VerifyTokenView SimpleJWTAdapterfor projects already usingdjangorestframework-simplejwt
Permissions¶
- Async-aware variants of all standard DRF permission classes with
ahas_permissionandahas_object_permission - Boolean combinators:
AND,OR,NOTfor composing permission rules without subclassing
Throttling¶
- Async-aware
SimpleRateThrottle,AnonRateThrottle,UserRateThrottle,ScopedRateThrottlewithaallow_requestandawait_
Pagination¶
AsyncPageNumberPagination,AsyncLimitOffsetPagination,AsyncCursorPaginationFastPageNumberPagination(omitscountfor performance)
Responses¶
NDJSONResponsefor newline-delimited JSON streamingStreamingJSONListResponsefor streaming a JSON arraySSEResponsefor Server-Sent Events with automaticX-Accel-Buffering: no
Exception handler¶
restflow_exception_handlerwith structured error codes alongside DRF's standard detail/code shape
Caching¶
- Async-aware cache key constructors
Spectacular (drf-spectacular integration)¶
RestflowAutoSchemaresolvingaction_configs,request_serializer_class/response_serializer_class, and per-action paginationadd_filterset_parameterspostprocessing hook that injects filter query parameters for any view declaringfilterset_class, including plainAPIView
Testing¶
AsyncAPIClientandAsyncRequestFactoryfor testing async views withoutsync_to_asyncwrappersAsyncAPITestCasebase class
[1.0.0a2] - 2025-12-03¶
Breaking Changes¶
- Renamed
lookup_exprtofilter_by: - All Field classes now use
filter_byparameter instead oflookup_exprfor defining filter behavior. Update all field definitions:lookup_expr="name__icontains"becomesfilter_by="name__icontains" -
Internal method
ensure_lookup_expr()renamed toensure_db_field() -
Removed
descriptionparameter: Field'sdescriptionparameter has been removed - Use Django REST Framework's
help_textparameter instead for field documentation
Added¶
- db_field parameter: New parameter for dynamic lookup field generation
- Allows creating filter fields with different names that map to the same database field
- Example:
product_price = IntegerField(db_field="price", lookups=["comparison"])creates multiple filters (product_price, product_price__gt, etc.) that all filter against the "price" database field -
Enables lookup generation when using
methodor customfilter_byfunctions -
Enhanced validation: Added validation to ensure
db_fieldis set when usinglookupswith custommethodorfilter_byparameters - Provides clear error messages with examples when validation fails
Changed¶
- Improved filter field handling with better separation between field name (API) and database field name (ORM queries)
- Enhanced error messages with more descriptive and actionable text
- Model-based field generation now automatically sets both
filter_byanddb_fieldparameters - Related field filtering (ForeignKey, OneToOneField) correctly sets both parameters
Documentation¶
- Updated all references from
lookup_exprtofilter_byacross documentation - Added comprehensive examples for
db_fieldparameter usage - Expanded FilterSet and Field guides with new parameter explanations
- Updated tutorial and quick start guides with new syntax
Migration from 1.0.0a1¶
- Replace
lookup_exprwithfilter_byin all FilterSet field definitions - Replace
descriptionparameter withhelp_textif used - Custom filter method signatures remain unchanged and compatible
[1.0.0a1] - 2025-11-25¶
Added¶
Core Features¶
- FilterSet: Declarative filtering system for Django REST Framework
- Field Types: Comprehensive set of filter fields
- StringField, IntegerField, FloatField, DecimalField
- BooleanField, DateField, DateTimeField, TimeField, DurationField
- ChoiceField, MultipleChoiceField
- EmailField, IPAddressField
- ListField for array filtering
- OrderField for result ordering
- RelatedField for related fields in models
- Field base class for custom filters
Declaration Styles¶
- Type annotation support (
name: str,price: int) - Explicit field declarations
- Model-based automatic field generation
- Mixed declaration styles
Lookup System¶
- Automatic lookup generation from field definitions
- Lookup categories (basic, text, comparison, date, time, postgres, pg_array)
- Custom lookup expressions via strings or callables
- Field variants (base field + lookups + negations)
Filtering Features¶
- Negation support via
!suffix - Multiple filter operators (AND, OR, XOR)
- Custom filter methods
- Preprocessors and postprocessors
- Related field filtering
Ordering¶
- OrderField for flexible result ordering
- Ascending/descending support
- Multiple field ordering
- Configurable ordering direction
PostgreSQL Support¶
- PostgreSQL array field filtering
- Array lookups (contains, overlaps, contained_by)
- Full-text search support
- Trigram similarity
Model Integration¶
- Automatic field generation from Django models
- Support for model field types
- ForeignKey and OneToOneField filtering
- Model choice field detection
Validation¶
- Built on DRF's validation system
- Automatic type conversion
- Field-level and custom validators
- Detailed error messages
Type Safety¶
- Python type hint support
- Automatic field type inference
- Type mapping for common Python types
- Literal type for choices
Documentation¶
- Comprehensive user guide
- API reference
- Quick start tutorial
- PostgreSQL guide
- Migration guide from django-filter
Testing¶
- Test suite with 95%+ coverage
- PostgreSQL-specific tests
- Multiple Python version support (3.10-3.14)
- Multiple Django version support (3.2-5.2)
- CI/CD with GitHub Actions
Developer Experience¶
- Modern Python features (type hints, dataclasses)
- Clear error messages
- Extensive docstrings
- IDE-friendly API
Version Support¶
| Version | Python | Django | DRF | Status |
|---|---|---|---|---|
| 1.0.0a1 | 3.10-3.14 | 3.2-5.2 | 3.14+ | Alpha |
Migration from django-filter¶
drf-restflow offers similar functionality to django-filter with a more modern, declarative API. Key differences:
- Type annotations: Use Python type hints instead of explicit field declarations
- Automatic negation: Built-in
!suffix support for all filters - Lookup categories: Group related lookups (e.g., "comparison" for gt/gte/lt/lte)
- Better validation: Integrated with DRF's validation system
For migration assistance, refer to the FilterSet Guide and Fields Guide for comprehensive documentation.
Deprecation Policy¶
Following semantic versioning:
- Major versions (x.0.0): May include breaking changes
- Minor versions (0.x.0): New features, backward compatible
- Patch versions (0.0.x): Bug fixes, backward compatible
Deprecation warnings will be issued for at least one minor version before removal.
Reporting Issues¶
Found a bug or have a feature request? Please open an issue on GitHub.
Contributing¶
See Contributing Guide for information on how to contribute to this changelog and the project.