Skip to content

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

python
@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

python
class _PublicIdQuerySetMixin(Generic[_ModelT])

from_public_id

python
def from_public_id(value: str) -> _ModelT | None

Return the row addressed by value within this queryset policy.

AngeeQuerySet

python
class AngeeQuerySet(_PublicIdQuerySetMixin[_ModelT], RebacQuerySet[_ModelT])

QuerySet API shared by Angee source and runtime models.

apply_ambient_scope

python
def apply_ambient_scope() -> Self

Eagerly apply REBAC row scope using the queryset or ambient actor.

lock_if_supported

python
def lock_if_supported(*, of: tuple[str, ...] = ("self", )) -> Self

Apply a self-scoped row lock only on database backends that support it.

locked_get

python
def locked_get(*args: Any, **kwargs: Any) -> _ModelT

Return one row under a database row lock when the backend supports it.

scoped_for_aggregate

python
def scoped_for_aggregate() -> Self

Return 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

python
class AngeeUnscopedQuerySet(_PublicIdQuerySetMixin[_ModelT],
                            models.QuerySet[_ModelT])

Angee queryset API for models that intentionally have no REBAC row policy.

scoped_for_aggregate

python
def scoped_for_aggregate() -> Self

Return 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

python
class AngeeManager(RebacManager.from_queryset(AngeeQuerySet))

Manager backed by AngeeQuerySet.

get_queryset

python
def get_queryset() -> AngeeQuerySet[Any]

Return the base Angee queryset for this manager's model.

check_create

python
def check_create(
        relationships: Mapping[str, Sequence[Any]] | None = None
) -> SubjectRef

Authorize 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

python
class AngeeUnscopedManager(models.Manager.from_queryset(AngeeUnscopedQuerySet)
                           )

Manager backed by AngeeUnscopedQuerySet.

get_queryset

python
def get_queryset() -> AngeeUnscopedQuerySet[Any]

Return the base unscoped Angee queryset for this manager's model.

AngeeModel

python
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

python
class Meta()

Django model options for Angee's abstract model base.

system_queryset

python
@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

python
@classmethod
def is_runtime_model(cls) -> bool

Return whether this model class declares itself as a runtime model.

overrides_runtime_parent

python
@classmethod
def overrides_runtime_parent(cls) -> bool

Return whether this materialized child opts into child-first emission.

is_catalogue_model

python
@classmethod
def is_catalogue_model(cls) -> bool

Return whether this class declares itself as catalogue data.

get_catalogue_tier

python
@classmethod
def get_catalogue_tier(cls) -> str

Return this class's declared catalogue tier, defaulting to master.

get_rebac_grantable

python
@classmethod
def get_rebac_grantable(cls) -> dict[str, str]

Return and validate this model's declared record-share relations.

record_access_permission

python
@classmethod
def record_access_permission(cls, relation: str) -> str

Return 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

python
def grant_record_access(relation: str,
                        subject: models.Model | SubjectRef) -> None

Idempotently grant subject one declared direct relation.

revoke_record_access

python
def revoke_record_access(relation: str,
                         subject: models.Model | SubjectRef) -> None

Idempotently revoke subject from one declared direct relation.

direct_record_access

python
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

python
@classmethod
def check(cls, **kwargs: Any) -> list[checks.CheckMessage]

Run Django model checks plus Angee structural declaration checks.

impl_key_for

python
@classmethod
def impl_key_for(cls,
                 field_name: str,
                 value: Any,
                 *,
                 default: str | None = None) -> str

Return the canonical registry key for one ImplClassField value.

resolve_impl_class

python
@classmethod
def resolve_impl_class(cls,
                       field_name: str,
                       value: Any,
                       *,
                       default: str | None = None) -> type

Return the impl class bound to one supplied impl-field value.

impl_field

python
@classmethod
def impl_field(cls, field_name: str) -> Any

Return 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

python
def resolve_impl(field_name: str, *, default: str | None = None) -> type

Return the impl class selected by field_name on this instance.

apply_create_defaults

python
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

python
@classmethod
def get_extension_target(cls) -> str | None

Return the normalized model label this source model extends.

get_extension_bases

python
@classmethod
def get_extension_bases(cls) -> tuple[type[models.Model], ...]

Return abstract model bases contributed by this extension.

public_id

python
@property
def public_id() -> str

Return the stable public identifier for this model instance.

from_public_id

python
@classmethod
def from_public_id(cls, value: str) -> Self | None

Return the instance addressed by value, if one exists.

public_id_lookup

python
@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

python
@classmethod
def public_id_from_pk(cls, value: Any) -> str

Return the public id encoded from this model's primary-key value.

public_id_value

python
def public_id_value() -> Any

Return the raw public identifier value owned by this instance.

broadcasts_changes

python
def broadcasts_changes() -> bool

Return 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

python
class AngeeDataModel(SqidMixin, AngeeModel)

Abstract base for Angee rows that participate in public data contracts.

Meta

python
class Meta()

Django model options for Angee's public data model base.

role_anchor

python
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).

Released under the AGPL-3.0 License.