Action Configuration
A single decorator, @action_configuration({...}), for giving context processors and command middleware per-viewset (or per-action) configuration - instead of a one-off boolean class attribute per concern (which doesn't scale once more than one processor/middleware needs this).
Where this fits
Session (see Authentication) is the first real user of this mechanism - see Architecture for why a one-off boolean class attribute per concern wouldn't have scaled, and how @action_configuration generalizes that into something any context processor or command middleware can use, for any concern.
The decorator
Works identically whether applied to a viewset class or to an individual method (typically a perform_* method, where the actual logic lives):
from fastapi_viewsets.action_configuration import action_configuration
@action_configuration({SomeIdentifier: "some-value", AnotherIdentifier: 5})
class ItemViewSet(...): ...
class ItemViewSet(...):
@action_configuration({SomeIdentifier: "some-value"})
async def perform_update(self, context, ...): ...The dict's keys are identifiers - a context processor callable already registered in settings.viewsets_context_processors, a Middleware subclass already registered in settings.viewsets_command_middleware (see Configuring an existing middleware below), or a fresh Middleware instance to inject just for this call (see Injecting a new middleware). The values are opaque - the framework never interprets them, only delivers them to whichever processor/middleware reads them.
Merge order: default → class → method
from fastapi_viewsets.conf import settings
settings.default_action_configuration = {SomeIdentifier: "fallback-value"}For a given call, configuration is merged per identifier - settings.default_action_configuration (lowest priority) → the viewset class's @action_configuration → the specific method's @action_configuration (highest priority). A method-level entry replaces the class-level entry for that identifier only - other identifiers the method doesn't mention still fall through from the class or the global default:
@action_configuration({Session: True, RateLimiter: 5})
class ItemViewSet(...):
@action_configuration({Session: False}) # only overrides Session for this one action
async def perform_create(self, context, data): ...
# every other action on this viewset: Session required, RateLimiter still 5Method-level lookup checks both the actual action name (for hand-written __router endpoints, which have no separate perform_* counterpart) and the perform_* method it maps to (update/ partial_update → perform_update, list_items → perform_list, etc.) - so decorating perform_update (what you actually write) works exactly as decorating update (the framework's own @final method) would.
Varying a value per action: ByAction
A single class-level entry can mean different things depending on which action is currently running:
from fastapi_viewsets.context import ByAction
@action_configuration({authz_context_processor: ByAction(list_items="read", update="write", default="read")})
class ItemViewSet(...): ...ByAction is resolved at read time (against whichever action a Context currently represents) rather than once when the configuration was merged - this matters for a hypothetical future WS transport, where one Context (via clone_for_command()) could serve several different actions on one long-lived connection; each read resolves fresh instead of reusing whatever the first action resolved to.
Receiving configuration in a context processor
A processor opts in by declaring a parameter literally named config (detected by name, the same convention this library uses for detecting context in route_viewset.py) - a processor that doesn't declare it is called exactly as before, unaffected:
async def authz_context_processor(request, viewset, config) -> dict:
return {"authz": config} # config is this processor's own resolved entry, or None if unconfigured
settings.viewsets_context_processors = [authz_context_processor]Receiving configuration in a command middleware
Middleware has a config_from(context) helper - shorthand for context.configuration_for(type(self)):
from fastapi_viewsets.middleware import Middleware, ViewSetResult
class RateLimiter(Middleware):
async def __call__(self, request, viewset, context, call_next):
config = self.config_from(context)
limit = self.default_limit if config is None else config
...
return await call_next()RateLimiter is a real, ready-to-use middleware shipped with this library - see Rate Limiter for the full implementation.
Configuring an existing middleware
Key the dict with the Middleware class to configure whichever instance is already registered globally:
settings.viewsets_command_middleware = [RateLimiter(default_limit=100)]
@action_configuration({RateLimiter: 10}) # this viewset gets a stricter limit
class ExpensiveViewSet(...): ...A class key with no matching instance anywhere in settings.viewsets_command_middleware raises ValueError at request time, fail-fast - almost certainly a typo or a forgotten global registration, not an intentional injection (use an instance for that - see below).
Injecting a new middleware, just for one viewset/method
Key the dict with a Middleware instance instead of a class - it's appended to the end of the chain (after the global list), only for calls that resolve this configuration:
@action_configuration({AuditLogMiddleware(): None})
class SensitiveViewSet(...): ...The instance is constructed once (at class-body/decoration time, like any decorator argument) and shared across every request to that viewset/method - same as a globally-registered middleware singleton would be.
Rejecting a request: just raise
There's no dedicated "reject" hook - a context processor or command middleware that wants to short-circuit with an error can simply raise fastapi.HTTPException. This isn't special machinery: an exception raised anywhere in the await chain that builds context or runs the middleware chain propagates straight out to FastAPI's own exception handling, exactly like it would from a plain endpoint function - save_state() (see ViewSet Lifecycle) still runs in its finally block either way. Command middleware also has the ViewSetResult(status_code=...) short-circuit shown above for Session - either approach works; raise when the rejection logic lives in a context processor (which has no ViewSetResult to return), use ViewSetResult when it lives in command middleware and you also want to shape headers/cookies on the way out.
Known limitations
- A config-dict identifier meant to name a context processor isn't validated against
settings.viewsets_context_processors(unlike theMiddleware-class case above) - identifiers are opaque and can legitimately be app-defined keys unrelated to any processor/middleware (read directly viacontext.configuration_for(...)by your own code), so there's no generic way to tell "this was supposed to match something" from "this is a bespoke key." __action_configuration__at the class level does not merge across inheritance - a subclass that redeclares@action_configurationfully replaces the base class's version (the same shadowing behaviour as any plain class attribute).
