Skip to content

angee.base.fields

Angee model field types.

Thin semantic wrappers over the libraries docs/stack.md names as the owner of each concern. Angee adds only the naming and the framework default; the library owns the behavior.

canonical_sqid_prefix

python
def canonical_sqid_prefix(prefix: str) -> str

Return prefix carrying Angee's public-id separator (abc -> abc_).

encode_public_id

python
def encode_public_id(sqids: Sqids, prefix: str, value: Any) -> str

Return the public id encoding value's backing integer under prefix.

The one reading of "encode a primary-key value to an Angee public id" — the shared body behind SqidField.public_id_from_value and SqidPublicIdentity.public_id_from_pk. prefix is already canonical.

SqidField

python
class SqidField(SqidsField)

Angee's opaque public id column, declared as django-sqids glue.

docs/stack.md names django-sqids the owner of opaque external ids; this wrapper makes the decoder total and lets a model state only the one fact that varies between models — the prefix. A model declares sqid_prefix = "nte_" (SqidMixin exposes the attribute and the shared column); the field reads it in contribute_to_class rather than every model re-declaring the whole column. An explicit prefix= still wins.

Totality: from_db_value receives None when the encoded column arrives through a nullable join — e.g. values_list("parent__sqid") over a nullable self-FK, the shape REBAC field-backed arrows query — and upstream encodes unconditionally there.

__init__

python
def __init__(*args: Any, prefix: str = "", **kwargs: Any) -> None

Normalize Angee public-id prefixes to the canonical abc_ shape.

contribute_to_class

python
def contribute_to_class(cls: type[models.Model], name: str) -> None

Resolve the prefix from the model's <field>_prefix when unset.

Lets SqidMixin's one shared column serve every model: each model states only sqid_prefix = "nte_" and the inherited field picks it up here. sqid is a private, non-concrete column, so this never reaches a migration — it only shapes how the id encodes.

deconstruct

python
def deconstruct() -> tuple[str | None, str, list[Any], dict[str, Any]]

Serialize the full public-id contract for generated/runtime models.

Emits the resolved prefix (not the declared one), so an emitted or migration-state model carries the full prefix without needing the source's sqid_prefix class attribute.

from_db_value

python
def from_db_value(value: Any, expression: Any, connection: Any, *args:
                  Any) -> Any

Return the encoded public id, passing NULL columns through.

django_sqids from_db_value encodes unconditionally, so a NULL arriving through a nullable join crashes it (sqids.encode([None]) raises TypeError); this guard is the workaround. The durable fix is an upstream django_sqids PR, after which this override can be deleted.

public_id_from_value

python
def public_id_from_value(value: Any) -> str

Return the encoded public id for one backing integer value.

StateField

python
class StateField(TextChoicesField)

A finite-state column backed by a TextChoices enum.

docs/stack.md names django-choices-field the owner of enum-backed model fields; this is the StateField semantic wrapper it lists. The enum is the single source of truth — strawberry-django emits the GraphQL enum straight from choices_enum and the column max_length is derived from it, so a state column never restates its choices. Declared natively, e.g. StateField(choices_enum=Note.Status, default=...).

__init__

python
def __init__(**kwargs: Any) -> None

Default a state column to indexed; it is what queries filter on.

to_python

python
def to_python(value: Any) -> Any

Accept stored values and GraphQL enum member names for this state.

pre_save

python
def pre_save(model_instance: models.Model, add: bool) -> Any

Normalize the in-memory value before Django writes and returns it.

EncryptedField

python
class EncryptedField(models.TextField)

Fernet-at-rest text field for framework secret values.

The database stores a Fernet token while Python reads return decrypted plaintext. Each column derives its Fernet key from settings.SECRET_KEY with HKDF-SHA256 using the model's label_lower plus field name as the per-column label. The field is secret-by-type: never put it on a GraphQL type. Fernet is non-deterministic, so the column is not queryable by value; get_or_create()/update_or_create() keyed on it and bulk_update() of it will raise, unique=True/primary_key=True are rejected at construction, and ordering or distinct on the column are meaningless. Today the key tracks SECRET_KEY, so rotating SECRET_KEY orphans existing ciphertext; ANGEE_FERNET_KEYS/MultiFernet is the future rotation path.

__init__

python
def __init__(*args: Any, **kwargs: Any) -> None

Reject uniqueness contracts Fernet ciphertext cannot enforce.

contribute_to_class

python
def contribute_to_class(cls: type[models.Model],
                        name: str,
                        private_only: bool = False) -> None

Store the deterministic per-column label once Django binds the field.

get_db_prep_save

python
def get_db_prep_save(value: Any, connection: Any) -> str | None

Encrypt plaintext for storage in the database column.

from_db_value

python
def from_db_value(value: str | None, expression: Any,
                  connection: Any) -> str | None

Decrypt database tokens back to plaintext.

get_lookup

python
def get_lookup(lookup_name: str) -> Any

Allow null checks only; encrypted values are not comparable.

ImplClassField

python
class ImplClassField(TextChoicesField)

A column naming a non-model implementation class by a short key.

The open-set tool from docs/backend/guidelines.md: one concrete model whose row selects a strategy/client/backend class that differs only in behaviour (e.g. a storage.Backend row → a StorageBackend subclass). registry_setting names the Django setting that maps keys to dotted import paths ({"local": "angee.storage.backends.LocalBackend"}); addons contribute their impls into it through autoconfig — the framework's composition seam. Every addon has contributed by the time the schema is produced, so the key set is closed: the column is a TextChoices enum built from the registered keys, and strawberry-django renders the GraphQL enum natively (this is a TextChoicesField, exactly like StateField). The registry must be non-empty — an addon whose impl set could otherwise be empty registers a noop/null-object default (storage's local; integrate's none) so a composition always has at least one selectable impl. The field resolves a row's key against the mapping and import_strings the composed, trusted path (never row text), checking it is a base_class subclass — the shape Angee already uses to resolve an addon's declared schemas reference; manage.py check validates every configured path up front. Keys must be identifier-safe (they become enum members). Parameterized like StateField: ImplClassField(base_class=StorageBackend, registry_setting="ANGEE_STORAGE_BACKEND_CLASSES"). Resolution returns the class; the owning model instantiates it, because the constructor contract — what the impl receives — belongs with the row's config and identity.

__init__

python
def __init__(*,
             base_class: type | None = None,
             registry_setting: str = "",
             **kwargs: Any) -> None

Bind the implementation base and build the enum from the registry keys.

deconstruct

python
def deconstruct() -> tuple[str, str, list[Any], dict[str, Any]]

Emit a plain varchar column; rebuild the enum from the setting on reconstruct.

The enum is the set of installed impls — a runtime composition fact, not a database fact — so the choices are dropped and only registry_setting (plus the fixed max_length) rides through. Adding or removing an impl therefore never churns a migration; base_class survives onto the live model field through deepcopy inheritance.

check

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

Validate the declaration and every configured impl path.

Imports each dotted path in the mapping and checks it against base_class, so a typo or a non-subclass fails manage.py check rather than a later row resolution. base_class is checked on the live model field (kept through deepcopy inheritance), not on migration-state copies.

resolve_class

python
def resolve_class(key: Any) -> type

Return the impl class the configured mapping binds to key.

key may be a plain string or the enum member this column reads back. Delegates the registry lookup + base_class check to the shared :func:~angee.base.registry.resolve_impl_class owner after canonicalizing the key, so the per-row column and the row-less selectors resolve identically.

key_for

python
def key_for(value: Any) -> str

Return the canonical registry key for a stored/input enum-ish value.

GraphQL reads TextChoices fields as enum member names (GITHUB) while the database and registry use the member values (github). The field owns that mapping, so callers canonicalize here before resolving or storing.

impl_choices

python
def impl_choices() -> list[dict[str, Any]]

Return pickable choices (key/label/icon/category/defaults) for the registry.

The registry key is authoritative — it is the enum value the column stores; the rest comes from the resolved ImplBase subclass. A non-ImplBase impl (a bare behaviour class) degrades to a label-only choice.

Released under the AGPL-3.0 License.