angee.base.models
Runtime model primitives shared by composed Angee applications.
CATALOGUE_TIERS
Resource tiers a catalogue model may declare.
Mirrors :class:angee.resources.tiers.ResourceTier, the authoritative resource tier owner. angee.base cannot import the resources addon without reversing the dependency direction, so the resources test suite pins these literals in sync.
DirectRecordAccess
@dataclass(frozen=True, slots=True)
class DirectRecordAccess()One direct declared-relation tuple on a shareable record.
EXTENSION_DONOR_STRUCTURAL_MEMBERS
Class-dict members that do not make a same-row extension donor semantic.
_PublicIdQuerySetMixin
class _PublicIdQuerySetMixin(Generic[_ModelT])from_public_id
def from_public_id(value: str) -> _ModelT | NoneReturn the row addressed by value within this queryset policy.
AngeeQuerySet
class AngeeQuerySet(_PublicIdQuerySetMixin[_ModelT], RebacQuerySet[_ModelT])QuerySet API shared by Angee source and runtime models.
apply_ambient_scope
def apply_ambient_scope() -> SelfEagerly apply REBAC row scope using the queryset or ambient actor.
lock_if_supported
def lock_if_supported(*, of: tuple[str, ...] = ("self", )) -> SelfApply a self-scoped row lock only on database backends that support it.
locked_get
def locked_get(*args: Any, **kwargs: Any) -> _ModelTReturn one row under a database row lock when the backend supports it.
scoped_for_aggregate
def scoped_for_aggregate() -> SelfReturn a row-scoped queryset safe for permission-naive aggregation.
Aggregate compilers run through .values()/.aggregate() shapes whose dict rows field-read redaction cannot touch, so field redaction is disabled and REBAC row scope is applied eagerly. It fails closed: a REBAC-typed model with no actor and no sudo bypass returns an empty queryset rather than leaking every row, independent of REBAC_STRICT_MODE. An explicit sudo — per-queryset .sudo() or an ambient system_context — aggregates across all rows, unscoped, by design.
AngeeUnscopedQuerySet
class AngeeUnscopedQuerySet(_PublicIdQuerySetMixin[_ModelT],
models.QuerySet[_ModelT])Angee queryset API for models that intentionally have no REBAC row policy.
scoped_for_aggregate
def scoped_for_aggregate() -> SelfReturn this queryset for permission-naive aggregation.
These querysets are only for Angee models without rebac_resource_type; row authorization has no model-owned policy to apply.
AngeeManager
class AngeeManager(RebacManager.from_queryset(AngeeQuerySet))Manager backed by AngeeQuerySet.
get_queryset
def get_queryset() -> AngeeQuerySet[Any]Return the base Angee queryset for this manager's model.
check_create
def check_create(
relationships: Mapping[str, Sequence[Any]] | None = None
) -> SubjectRefAuthorize the ambient actor to create one not-yet-persisted row.
The REBAC pre-save signal cannot evaluate a per-row create gate for a row that has no id yet, so manager factories preflight the schema's create permission with the relations the row would carry (rebac.check_new), run the insert under per-instance sudo, and re-bind the verified actor on the saved row with with_actor so the bypass ends with that one insert.
relationships values may be model instances or SubjectRefs; instances are resolved through their declared REBAC resource type. Returns the verified actor; raises MissingActorError without an ambient actor and PermissionDenied when the gate refuses.
AngeeUnscopedManager
class AngeeUnscopedManager(models.Manager.from_queryset(AngeeUnscopedQuerySet)
)Manager backed by AngeeUnscopedQuerySet.
get_queryset
def get_queryset() -> AngeeUnscopedQuerySet[Any]Return the base unscoped Angee queryset for this manager's model.
AngeeModel
class AngeeModel(TimestampMixin, RebacMixin)Abstract base model for Angee source and runtime models.
objects
Default REBAC manager with Angee queryset conveniences.
extends
Optional app_label.ModelName target this source model extends.
runtime
Whether this abstract source model materializes into the generated runtime.
The read is non-inherited: an abstract base can stay runtime = False and a concrete source subclass opts in by declaring runtime = True itself. Extensions use extends instead of this flag.
child_overrides_parent
Whether a materialized child's own methods override its concrete parent's.
A materialized child (runtime = True + extends) is emitted class Child(ConcreteParent, AbstractChild) — concrete parent first — so the parent wins the MRO and the child cannot override the parent's methods natively. Declaring child_overrides_parent = True flips this one child's base order to class Child(AbstractChild, ConcreteParent) so the child's own methods win. Read non-inherited (like runtime); the default preserves the safe parent-first status quo, so parties.Person/Organization (which declare a different default manager than Party) stay parent-first and byte-for-byte unchanged. The composer enforces the flip's manager/transition guards (see angee.compose.runtime).
catalogue
Whether this class declares itself as catalogue/reference data.
The read is non-inherited: a subclass must declare catalogue = True on its own class body to opt in, matching runtime's structural-marker shape.
catalogue_tier
Resource tier the catalogue rows belong to; read non-inherited.
rebac_grantable
Direct relations clients may manage, mapped to their required permission.
Models opt in explicitly, for example {"reader": "share"}. The composer carries the declaration onto the concrete runtime class and masks inherited declarations on materialized child models, so an undeclared model has no record-share surface.
Meta
class Meta()Django model options for Angee's abstract model base.
system_queryset
@classmethod
def system_queryset(
cls,
*,
using: str | None = None,
lock: tuple[str, ...] | None = None) -> AngeeQuerySet[Self]Return an elevated unscoped queryset with backend-gated locks; SQLite stays unlocked.
Locking was previously dead at these call sites and is now real.
is_runtime_model
@classmethod
def is_runtime_model(cls) -> boolReturn whether this model class declares itself as a runtime model.
overrides_runtime_parent
@classmethod
def overrides_runtime_parent(cls) -> boolReturn whether this materialized child opts into child-first emission.
is_catalogue_model
@classmethod
def is_catalogue_model(cls) -> boolReturn whether this class declares itself as catalogue data.
get_catalogue_tier
@classmethod
def get_catalogue_tier(cls) -> strReturn this class's declared catalogue tier, defaulting to master.
get_rebac_grantable
@classmethod
def get_rebac_grantable(cls) -> dict[str, str]Return and validate this model's declared record-share relations.
record_access_permission
@classmethod
def record_access_permission(cls, relation: str) -> strReturn the permission required to manage one declared relation.
An unknown relation is a hard error before any relationship tuple is constructed, making the share surface unable to mint undeclared tuples.
grant_record_access
def grant_record_access(relation: str,
subject: models.Model | SubjectRef) -> NoneIdempotently grant subject one declared direct relation.
revoke_record_access
def revoke_record_access(relation: str,
subject: models.Model | SubjectRef) -> NoneIdempotently revoke subject from one declared direct relation.
direct_record_access
def direct_record_access() -> tuple[DirectRecordAccess, ...]Return authorized direct tuples for this record's declared relations.
This deliberately reads only stored relationship rows. It does not walk usersets, groups, roles, relation arrows, or effective permissions.
check
@classmethod
def check(cls, **kwargs: Any) -> list[checks.CheckMessage]Run Django model checks plus Angee structural declaration checks.
impl_key_for
@classmethod
def impl_key_for(cls,
field_name: str,
value: Any,
*,
default: str | None = None) -> strReturn the canonical registry key for one ImplClassField value.
resolve_impl_class
@classmethod
def resolve_impl_class(cls,
field_name: str,
value: Any,
*,
default: str | None = None) -> typeReturn the impl class bound to one supplied impl-field value.
impl_field
@classmethod
def impl_field(cls, field_name: str) -> AnyReturn the declared ImplClassField named by field_name.
This is the model-owned accessor for callers that need the impl field's declared API without reaching through Django's raw _meta shape.
resolve_impl
def resolve_impl(field_name: str, *, default: str | None = None) -> typeReturn the impl class selected by field_name on this instance.
apply_create_defaults
def apply_create_defaults() -> Mapping[str, Sequence[Any]]Apply this row's blank-on-input create defaults before the create gate.
The auto-CRUD create preflight (AngeeManager.check_create via the Hasura write backend) evaluates the REBAC create permission against the unsaved instance before save() runs. A field a model defaults in save() — a blank-on-input scope relation derived from the actor, for example — is therefore still blank when the gate fires, so a create = scope->member arm fail-closes on a create that would in fact have persisted a scope.
A model that defaults a subject-bearing relation on save() overrides this hook to apply that default here too (idempotent with save(), so the row still persists with it) and return the relation contributions the default adds, keyed by relation name with subject values — so the gate is evaluated against the row as it will persist. The base default applies no defaults and contributes nothing.
get_extension_target
@classmethod
def get_extension_target(cls) -> str | NoneReturn the normalized model label this source model extends.
get_extension_bases
@classmethod
def get_extension_bases(cls) -> tuple[type[models.Model], ...]Return abstract model bases contributed by this extension.
public_id
@property
def public_id() -> strReturn the stable public identifier for this model instance.
from_public_id
@classmethod
def from_public_id(cls, value: str) -> Self | NoneReturn the instance addressed by value, if one exists.
public_id_lookup
@classmethod
def public_id_lookup(cls, value: str) -> dict[str, Any]Return the Django lookup for this model's public identifier.
public_id_from_pk
@classmethod
def public_id_from_pk(cls, value: Any) -> strReturn the public id encoded from this model's primary-key value.
public_id_value
def public_id_value() -> AnyReturn the raw public identifier value owned by this instance.
broadcasts_changes
def broadcasts_changes() -> boolReturn whether this row's saves/deletes broadcast on changes subscriptions.
The publisher (:mod:angee.graphql.publishing) asks each row this before emitting a change event, so a model can keep some rows off the generic model-change subscription surface entirely — the emission mirror of a get_queryset read scope that hides them from the list. Evaluated while the instance is still live (a delete carries the in-memory row), so the answer holds for deletes too, which a post-hoc queryset membership check could not decide. Defaults to broadcasting; a model that isolates rows to a record-scoped surface (record chatter reachable only through record_thread) overrides this to drop those rows.
AngeeDataModel
class AngeeDataModel(SqidMixin, AngeeModel)Abstract base for Angee rows that participate in public data contracts.
Meta
class Meta()Django model options for Angee's public data model base.
role_anchor
def role_anchor(resource_type: str,
*,
name: str | None = None,
module: str | None = None,
doc: str | None = None) -> type[AngeeModel]Return an abstract, table-less REBAC role anchor for resource_type.
A const-backed role relation (admin: <ns>/role // rebac:const=admin in an addon's permissions.zed) needs a model carrying that <ns>/rolerebac_resource_type so the rebac.E009 system check resolves the type; the anchor is managed = False (Django owns no table, there are never any rows) and runtime = True (the composer materializes it into the generated runtime, exactly like the hand-rolled anchors it replaces). One adopter declares its role in one line::
StorageRole = role_anchor("storage/role")
name defaults to a CamelCase of resource_type (storage/role -> StorageRole); pass it when the module symbol differs from that default (e.g. TagRole = role_anchor("tags/role", name="TagRole")). module defaults to the caller's module (sys._getframe) so the composer scans and imports the anchor from the adopting addon; the module symbol you bind must match name so the emitted from <addon>.models import <name> import resolves. Wrapper hazard: the frame default captures the direct caller, so a helper that wraps this factory would capture the helper's module, not the adopter's, and emit an import that resolves to the wrong symbol. Call role_anchor directly at module level, or pass module=__name__ when indirecting it. The composer verifies the captured module actually binds the anchor at emission (Runtime._class_import) and fails loudly on a mis-capture rather than emitting a broken import.
The .zed fragment stays co-located and static — each adopter ships its own definition <ns>/role block beside its models; the factory owns only the Django anchor model, never a composer .zed emission.
Adopters declare their role in one line beside their own models — for example, framework storage (StorageRole) and tags (TagRole).