Clauses API#

fastapi_restly.clauses implements composable, context-bound query clauses: named fragments of a query, declared once at module level, composed with boolean functions, and applied to plain SQLAlchemy statements.

Composable, context-bound query clauses for SQLAlchemy.

A WhereClause is a named, reusable predicate. where_clause() builds one from a SQLAlchemy condition or from a function that returns one, and all_of()/any_of()/none_of() compose them. A clause takes no arguments and holds no values.

A value that changes per request or per call is a ContextParam: a member of a ContextNamespace (name: ContextParam[T]), bound through the namespace (Current.bind(tenant_id=tid), or Current.depends(…) per request). Embedded in a condition (Item.tenant_id == Current.tenant_id) the member is a placeholder; called in a clause function (Current.tenant_id()) it returns the value. Both are read when the clause is applied, so the statement carries the values with it, and an unbound member raises LookupError instead of running unfiltered.

A WhereClause is also callable. Calling it returns the raw ColumnElement for use inside plain SQLAlchemy: join conditions, CASE expressions, or a hand-built .where(). This bypasses apply_clauses’ table validation, so raw SQLAlchemy rules apply.

Statement construction stays plain SQLAlchemy. Build select()/update()/ delete() as usual and pass the result through apply_clauses(), the bridge between the two worlds. A clause is a predicate and never joins: express a condition on a related table as EXISTS (relationship .any()/.has()). apply_clauses rejects a predicate that references a table the statement does not select from.

class fastapi_restly.clauses.ClauseNamespace#

Bases: object

Groups clauses under one named class.

Subclass and put the clauses in the class body; earlier names are available to later compositions. On definition the namespace validates that every public attribute is a clause (catching a bare SQLAlchemy expression that forgot its where_clause() wrapper). Usage is by class name (ItemClauses.visible): plain attribute access that any type checker follows. A clause needs no model; a namespace that declares model = <mapped class> is the namespace for that model and registers itself, so its default_scope reaches the model’s reads and reference checks; the model class itself is never touched. Define such a namespace in the model’s module, so importing the model guarantees the registration ran. A namespace without a model is a plain group: shared clauses, or a base class whose __init_subclass__ shapes the namespaces that extend it.

The name default_scope is reserved: a clause under that name is the scope every view read and every reference check on the model applies unless a view declares its own (see the Scopes guide). A join-dependent predicate is an EXISTS (.any()/.has()). A model subclass inherits the nearest declared default_scope along its MRO: a namespace that does not declare one leaves an inherited scope in force, and default_scope = UNSCOPED is the explicit opt-out. None says nothing here and is rejected.

default_scope: ClassVar[WhereClause | Unscoped] = fr.clauses.UNSCOPED#
model: ClassVar[type[Any]]#
class fastapi_restly.clauses.ContextNamespace#

Bases: object

Declares the ContextParams of one context, one per annotation.

Subclass and annotate: each public annotation name: ContextParam[T] materializes a ContextParam bound under name, so the attribute name is the bind name and a typo fails at import. Assigning an existing member adopts it (tenant_id = Current.tenant_id), so namespaces can share one member; helpers take a leading underscore. The conventional app-wide subclass is named Current; a value with a smaller audience gets a smaller namespace beside its consumers. Declaring here says where a value lives, not where it is bound: binding stays at the narrowest level that knows the value, and an unbound read still raises.

classmethod bind(**values: Any) → Iterator[None]#

Bind member values for the duration of the with block.

Keys are member names; a name this namespace does not declare raises. An inner bind replaces the value until its block exits. Two names for the same member raise TypeError before binding any values, even if the supplied values are equal.

classmethod depends(**sources: Any) → Any#

A FastAPI dependency that binds the named members per request.

Keyword names are member names; each value is the dependency the member’s value comes from (a callable, a Depends(...), or an Annotated alias), so app.dependency_overrides keeps working. The result drops into a dependencies=[...] list at app, router, or view level. It is an async dependency underneath: the bind lands in the request task, where async and def endpoints alike read it. Two names for the same member raise TypeError at declaration.

classmethod explain() → str#

Each member with its bound value and origin, or UNBOUND.

class fastapi_restly.clauses.ContextParam#

Bases: Generic[_T]

One value of a context: bound around a unit of work, read anywhere.

Declared in a ContextNamespace (tenant_id: ContextParam[UUID]), never constructed directly: the namespace is its address, and it is bound through the namespace’s bind() or depends(). One member, two positions. Called, it returns the bound value, or raises LookupError when nothing is bound. Embedded in a SQL expression (Item.tenant_id == Current.tenant_id) it becomes a placeholder, filled with the bound value each time a clause that carries it is applied.

A member never stands in for its value: comparisons with values and truth tests raise. Call the member to read it, and in a SQL expression put the column first (Item.role == Current.role).

__call__() → _T#

The bound value; LookupError when nothing is bound.

class fastapi_restly.clauses.Unscoped#

Bases: object

Sentinel scope: explicitly no scope, everywhere a scope can appear.

UNSCOPED is the one instance; the class is public so a scope declaration or a get_one / get_many override can name the type (WhereClause | Unscoped). The explicit spelling for a view’s scope (where None means “fall back to the model’s default”), a namespace’s default_scope (where undeclared defers to a base model’s namespace, or none), and a RefExists scope (where None is rejected, so a variable that happens to be None can never silently unscope). apply_clauses() accepts it and applies nothing for it, preserving existing filters and other supplied clauses. Boolean composition treats it as SQL TRUE: all_of ignores it and returns the sole remaining clause unchanged, or UNSCOPED when none remain. any_of propagates it, and none_of returns a WhereClause over SQL false(). Composition validates other operands before simplifying. Searching for UNSCOPED finds explicit uses. Variables and composition can pass the sentinel to other scope declarations and read calls. Deliberately not re-exported at the top level: the escape is spelled in full.

class fastapi_restly.clauses.WhereClause#

Bases: object

A named, reusable predicate.

Built by where_clause() and by all_of()/any_of()/none_of(), never constructed directly. It takes no arguments and holds no values: a value that changes per request or per call is a ContextNamespace member, embedded in the condition or read in the clause function. It applies to SELECT, UPDATE and DELETE statements through apply_clauses(), and calling it returns the raw ColumnElement for use inside plain SQLAlchemy expressions.

__call__() → ColumnElement[bool]#

Resolve to the raw ColumnElement, for plain SQLAlchemy use.

Context members are read now: an unbound one raises LookupError. The result drops into any expression position: .where(), a join condition, a CASE. This path skips apply_clauses’ table validation.

fastapi_restly.clauses.all_of(*clauses: WhereClause | Unscoped) → WhereClause | Unscoped#

AND the operands’ predicates.

UNSCOPED is a no-op. One remaining operand is returned unchanged, and none returns UNSCOPED. Calling with no arguments raises.

fastapi_restly.clauses.any_of(*clauses: WhereClause | Unscoped) → WhereClause | Unscoped#

OR the operands’ predicates.

UNSCOPED accepts every row, so any UNSCOPED operand returns the sentinel. Other operands are validated before this simplification and are not resolved if the result is UNSCOPED. Calling with no arguments raises.

fastapi_restly.clauses.apply_clauses(stmt, /, *clauses: WhereClause | Unscoped)#

Apply clauses to a statement built with plain SQLAlchemy.

The bridge between the two worlds: build select()/update()/delete() as usual, then let this add the clauses’ predicates to its WHERE. A predicate that references a table the statement does not select from is rejected: the silent alternative is a cartesian product. Express a condition on a related table as EXISTS (.any()/.has()). UNSCOPED among the clauses applies nothing: a scope seam passes on what it was given.

fastapi_restly.clauses.none_of(*clauses: WhereClause | Unscoped) → WhereClause#

True when none of the operands’ predicates hold.

NOT over the OR of all operands. An UNSCOPED operand returns a WhereClause over SQL false(), matching no rows. Other operands are validated but not resolved in that case.

fastapi_restly.clauses.where_clause(condition: ColumnElement[bool] | Callable[[], ColumnElement[bool]]) → WhereClause#

A WhereClause from a condition or a function returning one.

A ready-made ColumnElement is reused as-is. A function takes no parameters and runs each time the clause is applied, so it can branch in Python. Both read request and local values from ContextNamespace members: embed the member in the condition (Item.user_id == Current.user_id), or call it in the function body (Current.user_id()). A value the caller already has needs no clause function: build the condition there and wrap it, where_clause(Item.collection_id == collection_id).

In a ClauseNamespace body, stack this decorator over staticmethod: the marker keeps a type checker from reading the def as a method, and is unwrapped here.

fastapi_restly.clauses.UNSCOPED = fr.clauses.UNSCOPED#

Sentinel scope: explicitly no scope, everywhere a scope can appear.

UNSCOPED is the one instance; the class is public so a scope declaration or a get_one / get_many override can name the type (WhereClause | Unscoped). The explicit spelling for a view’s scope (where None means “fall back to the model’s default”), a namespace’s default_scope (where undeclared defers to a base model’s namespace, or none), and a RefExists scope (where None is rejected, so a variable that happens to be None can never silently unscope). apply_clauses() accepts it and applies nothing for it, preserving existing filters and other supplied clauses. Boolean composition treats it as SQL TRUE: all_of ignores it and returns the sole remaining clause unchanged, or UNSCOPED when none remain. any_of propagates it, and none_of returns a WhereClause over SQL false(). Composition validates other operands before simplifying. Searching for UNSCOPED finds explicit uses. Variables and composition can pass the sentinel to other scope declarations and read calls. Deliberately not re-exported at the top level: the escape is spelled in full.

See also

Current context covers request values and dependency binding. Query Clauses covers query fragments and composition.