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:
ModelAbstract 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.
Nonefor root nodes.on_deletedefaults toCASCADE; override withclade.deletion.ADOPTto re-parent children on deletion.- pathLtreeField
Dot-separated integer PK chain (e.g.
"1.2.4.6"). Stored asltreeon PostgreSQL,VARCHARelsewhere. Populated automatically by the post_save signal (clade.signals). Do not write directly.
Manager¶
objectsis aNodeManagerexposing 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
pathproduces 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.HeterogeneousAffinityErrorif the result would span more than one partner model — useaffinities_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 — seeclade.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=2corresponds to genealogical “1st cousin”. Delegates totype(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¶
Trueif this node has no descendants.
- property is_root: bool¶
Trueif 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).
Manager and QuerySet¶
- class clade.managers.NodeManager(*args, **kwargs)[source]¶
Bases:
ManagerManager that surfaces NodeQuerySet methods at the class level.
- class clade.managers.NodeQuerySet(model=None, query=None, using=None, hints=None)[source]¶
Bases:
QuerySetQuerySet 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 theancestor_oflookup registered onLtreeField.On other backends, builds a Python list of ancestor paths and issues a single
INquery.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=2corresponds to genealogical “1st cousin”;degree=3to “2nd cousin”.degree=1is degenerate and returns the same set assiblings_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 thedescendant_oflookup registered onLtreeField.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 existingsiblings_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).
Deletion¶
- clade.deletion.ADOPT¶
Singleton callable — use this in ForeignKey
on_deletearguments.
Fields¶
- class clade.fields.LtreeField(*args, db_collation=None, **kwargs)[source]¶
Bases:
CharFieldMaterialized path field — ltree on PostgreSQL, VARCHAR elsewhere.
Extends
CharFieldso that all Django ORM lookups (startswith,in,exact) work transparently on every backend without modification toNodeQuerySet.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
LtreeFieldis allowed perCladeNodesubclass.NodeQuerySetlocates 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 toCharField(i.e.VARCHAR(max_length)) on all other backends.
- deconstruct()[source]¶
Return field deconstruction for migration serialisation.
Overrides the path to
clade.fields.LtreeFieldso 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 itsdata_typesregistry. On non-PostgreSQL backends the field is stored as VARCHAR; on PostgreSQLdb_type()overrides this with"ltree".Migration detection (
makemigrations) relies ondb_type()returning"ltree"on PostgreSQL, not onget_internal_type().
- class clade.fields.ConditionalAlterField(*args, **kwargs)[source]¶
Bases:
AlterFieldMigration operation that alters a field only on supported backends.
Wraps Django’s
AlterFieldand skips the DDL entirely on backends that do not support the target field type (e.g. SQLite when the target isLtreeField).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
LtreeFieldappears 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.
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 toForeignKey(to=...)). Resolved lazily viaapps.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_ruleslist (enforced byclade.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
channelname (DD-018, v0.6.0). A model’s participation in a channel is only unlocked for chaining once everyAffinityRuletouching it under that name carriesshared=True– one-sided consent still failsclade.E003. Purely declarative here: this flag has no effect on rule resolution or signal wiring by itself, it is read byclade.E003and the closure engine (_recompute_shared_closure(), this module).
Storage¶
- class clade.affinity.Affinity(*args, **kwargs)[source]¶
Bases:
ModelA 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_deriveddistinguishes 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
QuerySetof node’s Affinity partners.Raises
HeterogeneousAffinityErrorif the matching rows reference more than one distinct partner model — useaffinities_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 emptyAffinityQuerySet is returned as a documented fallback. This is behaviourally indistinguishable from any other empty QuerySet for iteration,.exists(),.count(), etc. — only.modelreportsAffinityrather 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
Affinityrow. In the common case (single declaring source, or achannelnarrowing 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).