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
def canonical_sqid_prefix(prefix: str) -> strReturn prefix carrying Angee's public-id separator (abc -> abc_).
encode_public_id
def encode_public_id(sqids: Sqids, prefix: str, value: Any) -> strReturn 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
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__
def __init__(*args: Any, prefix: str = "", **kwargs: Any) -> NoneNormalize Angee public-id prefixes to the canonical abc_ shape.
contribute_to_class
def contribute_to_class(cls: type[models.Model], name: str) -> NoneResolve 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
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
def from_db_value(value: Any, expression: Any, connection: Any, *args:
Any) -> AnyReturn 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
def public_id_from_value(value: Any) -> strReturn the encoded public id for one backing integer value.
StateField
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__
def __init__(**kwargs: Any) -> NoneDefault a state column to indexed; it is what queries filter on.
to_python
def to_python(value: Any) -> AnyAccept stored values and GraphQL enum member names for this state.
pre_save
def pre_save(model_instance: models.Model, add: bool) -> AnyNormalize the in-memory value before Django writes and returns it.
EncryptedField
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__
def __init__(*args: Any, **kwargs: Any) -> NoneReject uniqueness contracts Fernet ciphertext cannot enforce.
contribute_to_class
def contribute_to_class(cls: type[models.Model],
name: str,
private_only: bool = False) -> NoneStore the deterministic per-column label once Django binds the field.
get_db_prep_save
def get_db_prep_save(value: Any, connection: Any) -> str | NoneEncrypt plaintext for storage in the database column.
from_db_value
def from_db_value(value: str | None, expression: Any,
connection: Any) -> str | NoneDecrypt database tokens back to plaintext.
get_lookup
def get_lookup(lookup_name: str) -> AnyAllow null checks only; encrypted values are not comparable.
ImplClassField
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__
def __init__(*,
base_class: type | None = None,
registry_setting: str = "",
**kwargs: Any) -> NoneBind the implementation base and build the enum from the registry keys.
deconstruct
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
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
def resolve_class(key: Any) -> typeReturn 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
def key_for(value: Any) -> strReturn 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
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.