API Reference

This page is generated from source docstrings via sphinx.ext.autodoc and sphinx.ext.napoleon (DD-017). It documents Clade’s public surface only — internal wiring (signal registration, django.core.checks hooks, PostgreSQL-only lookup/expression classes) is deliberately left out; see the narrative guides (The Node Tree, Kinship, Affinity) for how those pieces fit together.

Tree & hierarchy

CladeNode

class clade.models.CladeNode(*args, **kwargs)[source]

Bases: Model

Abstract base class for hierarchical (tree) models.

Subclass this to create a concrete hierarchical model:

class Department(CladeNode):
    name = models.CharField(max_length=255)

The module manages all hierarchy behaviour — queries, path maintenance, and deletion strategy. User code focuses exclusively on domain fields.

Fields

parentForeignKey (self, nullable)

Direct ancestor. None for root nodes. on_delete defaults to CASCADE; override with clade.deletion.ADOPT to re-parent children on deletion.

pathLtreeField

Dot-separated integer PK chain (e.g. "1.2.4.6"). Stored as ltree on PostgreSQL, VARCHAR elsewhere. Populated automatically by the post_save signal (clade.signals). Do not write directly.

Manager

objects is a NodeManager exposing hierarchy queries:

Department.objects.ancestors_of(node)
Department.objects.descendants_of(node)
Department.objects.siblings_of(node)
Department.objects.root_of(node)
Department.objects.piblings_of(node)
Department.objects.niblings_of(node)
Department.objects.cousins_of(node, degree=2)

Instance methods

Convenience wrappers that delegate to the manager:

node.ancestors()       → QuerySet
node.descendants()     → QuerySet
node.siblings()        → QuerySet
node.root              → single instance (property)
node.is_root           → bool (property)
node.is_leaf           → bool (property)
node.piblings()        → QuerySet
node.niblings()        → QuerySet
node.cousins(degree=2) → QuerySet
node.affinities(channel=None)         → QuerySet (DD-005, #70)
node.affinities_grouped(channel=None) → dict[type, QuerySet]

Ordering

Default ordering by path produces consistent depth-first traversal on SQLite and PostgreSQL backends.

affinities(channel: str | None = None)[source]

Return a single QuerySet of this node’s Affinity partners.

Delegates to type(self).objects.affinities_of(self, channel=...).

Raises clade.affinity.HeterogeneousAffinityError if the result would span more than one partner model — use affinities_grouped() in that case.

affinities_grouped(channel: str | None = None)[source]

Return {partner_model: QuerySet} of this node’s Affinity partners, grouped by model. Never raises — see clade.affinity.affinities_of_grouped().

Delegates to type(self).objects.affinities_of_grouped(self, channel=...).

ancestors()[source]

Return all ancestors as an unordered QuerySet.

Delegates to type(self).objects.ancestors_of(self). Apply .order_by('path') for root-first ordering.

cousins(degree: int = 2)[source]

Return nodes sharing a common ancestor degree levels above this node, at the same depth (symmetric degree — see DD-016).

degree=2 corresponds to genealogical “1st cousin”. Delegates to type(self).objects.cousins_of(self, degree=degree).

Returns an empty QuerySet if this node has no ancestor degree levels up.

descendants()[source]

Return all descendants as an unordered QuerySet.

Delegates to type(self).objects.descendants_of(self). Apply .order_by('path') for depth-first ordering.

property is_leaf: bool

True if this node has no descendants.

property is_root: bool

True if this node has no parent.

niblings()[source]

Return children of this node’s siblings as a QuerySet.

Gender-neutral for nephew/niece. Fixed degree only in v0.4.0 (see DD-016). Delegates to type(self).objects.niblings_of(self).

Returns an empty QuerySet if this node has no siblings, or if none of its siblings have children.

piblings()[source]

Return siblings of this node’s parent as a QuerySet.

Gender-neutral for aunt/uncle. Fixed degree only in v0.4.0 (see DD-016). Delegates to type(self).objects.piblings_of(self).

Returns an empty QuerySet if this node is a root (no parent).

property root

Return the root node of this tree (self if already root).

siblings()[source]

Return sibling nodes as a QuerySet (self excluded).

Delegates to type(self).objects.siblings_of(self).

Manager and QuerySet

class clade.managers.NodeManager(*args, **kwargs)[source]

Bases: Manager

Manager that surfaces NodeQuerySet methods at the class level.

get_queryset()[source]

Return a new QuerySet object. Subclasses can override this method to customize the behavior of the Manager.

class clade.managers.NodeQuerySet(model=None, query=None, using=None, hints=None)[source]

Bases: QuerySet

QuerySet with hierarchy traversal for CladeNode subclasses.

ancestors_of(node)[source]

Return all ancestors of node (root to direct parent).

On PostgreSQL, uses the native ltree @> operator via the ancestor_of lookup registered on LtreeField.

On other backends, builds a Python list of ancestor paths and issues a single IN query.

Returns an unordered QuerySet; call .order_by(field_name) for root-first ordering.

Returns an empty QuerySet for root nodes (no ancestors).

cousins_of(node, degree: int = 2)[source]

Return nodes sharing a common ancestor exactly degree levels above node, at the same depth as node (symmetric degree, not genealogical degree/removed — see DD-016).

degree=2 corresponds to genealogical “1st cousin”; degree=3 to “2nd cousin”. degree=1 is degenerate and returns the same set as siblings_of().

This definition is symmetric: it does not cover genealogical “removed” cousins (candidates at a different depth than node that share a common ancestor at an equivalent distance). That parameter is deferred to post-v1.0.0 (see DD-016).

Returns an empty QuerySet if node has no ancestor degree levels up (i.e. node is too close to the root).

Raises:

ValueError – If degree is less than 1.

descendants_of(node)[source]

Return all descendants of node (children, grandchildren, …).

On PostgreSQL, uses the native ltree <@ operator via the descendant_of lookup registered on LtreeField.

On other backends, uses a prefix search on the path field — single SQL statement.

Returns an unordered QuerySet; call .order_by(field_name) for depth-first ordering.

Returns an empty QuerySet for leaf nodes.

niblings_of(node)[source]

Return children of node’s siblings (gender-neutral nephew/niece).

Fixed degree only in v0.4.0 (see DD-016). Implemented as filter(parent__in=siblings_of(node)) — introduces no new SQL beyond the existing siblings_of() primitive.

Returns an empty QuerySet if node has no siblings, or if none of node’s siblings have children.

piblings_of(node)[source]

Return siblings of node’s parent (gender-neutral aunt/uncle).

Fixed degree only in v0.4.0 — no “grand-pibling” (see DD-016). Delegates entirely to siblings_of(); introduces no new SQL.

Returns an empty QuerySet if node is a root (no parent).

root_of(node)[source]

Return the root of the tree containing node.

If node is already the root, returns a QuerySet containing node itself.

siblings_of(node)[source]

Return nodes sharing the same parent as node.

node itself is excluded from the result.

Returns an empty QuerySet for root nodes (no common parent) and for only-children.

Deletion

clade.deletion.ADOPT

Singleton callable — use this in ForeignKey on_delete arguments.

Fields

class clade.fields.LtreeField(*args, db_collation=None, **kwargs)[source]

Bases: CharField

Materialized path field — ltree on PostgreSQL, VARCHAR elsewhere.

Extends CharField so that all Django ORM lookups (startswith, in, exact) work transparently on every backend without modification to NodeQuerySet.

On PostgreSQL, db_type() returns "ltree", enabling:

  • Native ltree indexing (GiST / GIN) — activated at v0.8.0.

  • Native ltree operators in NodeQuerySet — introduced at v0.3.0.

On all other backends (SQLite, MySQL, …), the field behaves as a plain VARCHAR.

Exactly one LtreeField is allowed per CladeNode subclass. NodeQuerySet locates it dynamically via _meta.get_fields() so the field may be renamed in subclasses without breaking clade.

Usage

Declared once on CladeNode.path — managed by the module. Do not instantiate directly in user code.

db_type(connection) str[source]

Return the database column type.

Returns "ltree" on PostgreSQL; delegates to CharField (i.e. VARCHAR(max_length)) on all other backends.

deconstruct()[source]

Return field deconstruction for migration serialisation.

Overrides the path to clade.fields.LtreeField so that generated migrations import from the correct location.

get_internal_type() Literal['CharField'][source]

Report as CharField for cross-backend compatibility.

Returning "CharField" ensures Django can resolve the correct SQL column type on every backend via its data_types registry. On non-PostgreSQL backends the field is stored as VARCHAR; on PostgreSQL db_type() overrides this with "ltree".

Migration detection (makemigrations) relies on db_type() returning "ltree" on PostgreSQL, not on get_internal_type().

class clade.fields.ConditionalAlterField(*args, **kwargs)[source]

Bases: AlterField

Migration operation that alters a field only on supported backends.

Wraps Django’s AlterField and skips the DDL entirely on backends that do not support the target field type (e.g. SQLite when the target is LtreeField).

The migration graph remains consistent on all backends — Django records the operation as applied — but only the supported backends receive the actual schema change.

This is the correct operation to use whenever a LtreeField appears in a migration, ensuring that:

  • PostgreSQL receives ALTER COLUMN path TYPE ltree USING path::ltree.

  • SQLite and other backends skip the DDL without error or table recreation.

Usage

In any migration involving a LtreeField:

from clade.fields import ConditionalAlterField, LtreeField

class Migration(migrations.Migration):
    operations = [
        ConditionalAlterField(
            model_name="department",
            name="path",
            field=LtreeField(
                max_length=255,
                blank=True,
                editable=False,
                db_index=True,
            ),
        ),
    ]
param vendors:

Database vendor names on which the operation is executed. Defaults to ("postgresql",). Pass additional vendors if future backends gain ltree support.

type vendors:

tuple[str, …]

database_backwards(app_label, schema_editor, from_state, to_state)[source]

Reverse the AlterField only on supported backends.

database_forwards(app_label, schema_editor, from_state, to_state)[source]

Execute the AlterField only on supported backends.

deconstruct()[source]

Return operation deconstruction for migration serialisation.

describe() str[source]

Return a human-readable description for showmigrations.

Affinity

Declaration

class clade.affinity.AffinityRule(local_field: str, *, to: str, target_field: str, channel: str, shared: bool = False)[source]

Declarative Affinity rule, registered via Meta.affinity_rules.

Follows the same declarative pattern as Meta.constraints:

class Department(CladeNode):
    region = models.CharField(...)

    class Meta(CladeNode.Meta):
        affinity_rules = [
            AffinityRule(
                "region", to="projects.Project",
                target_field="cost_center", channel="geo",
            ),
        ]
Parameters:
  • local_field (str) – Name of the scalar field on the declaring model.

  • to (str) – Target model, using the "app_label.Model" string convention (identical to ForeignKey(to=...)). Resolved lazily via apps.get_model() — never at declaration time, so no hard import coupling between user apps.

  • target_field (str) – Explicit field name on the target model. Never inferred by same-name matching (DD-005: two unrelated models sharing a field name, e.g. name, must not silently enter Affinity).

  • channel (str) – Free-form label identifying the rule and, transitively, its target model. Must be unique within a single model’s affinity_rules list (enforced by clade.E001) — reusable across different source models.

  • shared (bool, default False) – Opt-in consent, per rule, to let this model act as a pivot for declared-rule graph closure under this channel name (DD-018, v0.6.0). A model’s participation in a channel is only unlocked for chaining once every AffinityRule touching it under that name carries shared=True – one-sided consent still fails clade.E003. Purely declarative here: this flag has no effect on rule resolution or signal wiring by itself, it is read by clade.E003 and the closure engine (_recompute_shared_closure(), this module).

get_target_model() type[Model][source]

Resolve to to a concrete model class via apps.get_model().

Lazy by design — called only when a rule is actually evaluated (registry construction, checks, signal handling), never at declaration time.

Storage

class clade.affinity.Affinity(*args, **kwargs)[source]

Bases: Model

A single materialised Affinity relationship between two nodes.

One row per pair (not two mirrored rows): node.affinities() reads across both sides, so consistency has exactly one row to maintain per relationship rather than two that could drift apart.

is_derived distinguishes a direct pair (materialised by the v0.5.0 signal handlers below, DD-005) from a derived pair produced by the declared-rule graph closure (DD-018, v0.6.0). Both kinds share this single table — no separate model.

Do not create or update instances directly — maintained exclusively by the signal handlers wired via register_affinity_signals() (direct rows) and _recompute_shared_closure() below (derived rows).

exception DoesNotExist

Bases: ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

exception NotUpdated

Bases: ObjectNotUpdated, DatabaseError

Querying

clade.affinity.affinities_of(node: Model, channel: str | None = None) QuerySet[Model][source]

Return a single QuerySet of node’s Affinity partners.

Raises HeterogeneousAffinityError if the matching rows reference more than one distinct partner model — use affinities_of_grouped() in that case instead.

When no matching row exists, the result is an empty QuerySet. Its model is inferred from node’s own declared rule for channel when possible (giving a correctly-typed empty QuerySet); if that cannot be determined (no channel, or node declares no matching rule — e.g. node is a passive target with no Affinity rows yet), an empty Affinity QuerySet is returned as a documented fallback. This is behaviourally indistinguishable from any other empty QuerySet for iteration, .exists(), .count(), etc. — only .model reports Affinity rather than the (undeterminable) partner type.

clade.affinity.affinities_of_grouped(node: Model, channel: str | None = None) dict[type[Model], QuerySet[Model]][source]

Return {partner_model: QuerySet} for every partner of node.

Never raises — the dict has one key per distinct model found on the “other side” of a matching Affinity row. In the common case (single declaring source, or a channel narrowing to one rule), the dict has exactly one key. Empty dict if node has no Affinity rows (optionally, for channel).

exception clade.affinity.HeterogeneousAffinityError[source]

Raised by affinities_of() when partners span more than one model.

Use affinities_of_grouped() instead when that is expected — e.g. two different source models reusing the same channel name toward the same target instance (DD-005: channel uniqueness is per declaring model, not global).