Source code for fsh_lib.actions

from collections.abc import Awaitable, Callable, Collection, Iterable
from dataclasses import dataclass
from typing import Any, Literal, Self

from pydantic import BaseModel

Scope = Literal["object", "collection"]

# Signature every guard conforms to: ``(resource, auth) -> bool``.
# ``resource`` is the SQLAlchemy instance for object-scope actions
# or ``None`` for collection-scope actions; ``auth`` is whatever
# the consumer's auth dep resolves -- the type is left open since
# consumers pick the auth-context model.
CanCallable = Callable[[Any, Any], Awaitable[bool]]


[docs] class ActionRef[NameT: str = str, ScopeT: str = Scope](BaseModel): name: NameT scope: ScopeT formats: list[str] | None = None async_formats: list[str] | None = None
[docs] @dataclass(frozen=True) class ActionSpec: action: Action can: CanCallable is_object_action: bool formats: tuple[str, ...] | None = None async_formats: tuple[str, ...] | None = None @property def name(self) -> str: return self.action.operation @property def scope(self) -> Scope: return "object" if self.is_object_action else "collection"
[docs] class Action(str): __slots__ = ("operation", "resource") resource: str operation: str def __new__(cls, resource: str, operation: str) -> Self: self = super().__new__(cls, f"{resource}:{operation}") self.resource = resource self.operation = operation return self def __reduce__(self) -> tuple[Any, ...]: # str.__new__ takes one arg; Action.__new__ takes two, so teach # copy/pickle to rebuild from the resource + operation. return (Action, (self.resource, self.operation)) def __repr__(self) -> str: return ( f"Action(resource={self.resource!r}, operation={self.operation!r})" )
async def always_true(_resource: Any, _auth: Any) -> bool: # noqa: ANN401 return True async def available_actions[T: BaseModel = ActionRef]( resource: Any, # noqa: ANN401 auth: Any, # noqa: ANN401 action_specs: Iterable[ActionSpec], ref_cls: type[T] = ActionRef, # type: ignore[assignment] allowed: Collection[str] | None = None, ) -> list[T]: refs: list[T] = [] for spec in action_specs: if allowed is not None and spec.name not in allowed: continue if not await spec.can(resource, auth): continue kwargs: dict[str, Any] = {"name": spec.name, "scope": spec.scope} for field in ("formats", "async_formats"): val = getattr(spec, field) if val is not None: kwargs[field] = list(val) if isinstance(val, tuple) else val refs.append(ref_cls(**kwargs)) return refs def find_can( specs: Iterable[ActionSpec], name: str, ) -> CanCallable: for spec in specs: if spec.name == name: return spec.can msg = f"No action {name!r} in registry" raise KeyError(msg)