tangl.core identity

Identity, selection, and registry primitives used throughout the engine.

Related design docs

Related notes

Identity and lookup

class Entity(_ctx=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None)[source]

Canonical concrete core entity composed from identity + constructor-form traits.

Why

Entity is intentionally minimal. It adds no persistent fields beyond Unstructurable and HasIdentity and exists mainly to:

  • fix the default trait composition order for core entities, and

  • inject lifecycle dispatch hooks during creation paths.

Notes

Inheritance order is (Unstructurable, HasNamespace, HasIdentity). HasNamespace adds namespace contribution behavior, while __eq__ still compares by value via Unstructurable.eq_by_value().

  • Two entities with the same uid but different constructor-form values are not equal under ==.

  • Use HasIdentity.eq_by_id() when you need identity-only comparison.

Dispatch hook control signal

Pass _ctx to __init__ or structure() to activate dispatch hooks. The underscore prefix means this is a control argument and is not stored as model data. If a subclass also has a ctx data field, both can coexist:

CallReceipt(result=5, ctx=context, _ctx=context)

Example

>>> e = Entity(label="abc")
>>> f = Entity(uid=e.uid, label=e.label)
>>> e is not f and e.eq_by_value(f) and e == f
True
>>> e.eq_by_id(f)
True
>>> g = Entity(uid=e.uid, label="different")
>>> e.eq_by_id(g) and e != g
True

See also

tangl.core.dispatch

Hook registration and execution behavior.

tangl.core.ctx

Ambient context helpers for hook propagation.

class Selector(*, predicate=None, **extra_data)[source]

Pure query predicate model for matching entities.

Why

Selectors decouple query criteria from entity implementation. Entities expose attributes and predicate-like methods; selectors provide reusable matching logic.

Key Features

  • Selector is a Pydantic model, not an Entity.

  • predicate stores an optional custom callable pre-check.

  • With extra="allow", all criteria kwargs beyond predicate are stored in __pydantic_extra__ and interpreted by matches().

Notes

  • Criterion value typing.Any is a wildcard and is skipped.

  • None is not a wildcard; it is compared normally.

  • Any callable entity attribute is invoked with the criterion value, not only has_* / is_* names (those are conventions, not requirements).

  • Avoid properties that return callables on matchable names because selector callable detection is based on runtime callable(...) checks.

Example

>>> class E(Entity):
...     @property
...     def label_rev(self):
...         return self.label[::-1]
>>> e = E(label="abc")
>>> Selector(
...     predicate=lambda x: x.label == "abc",
...     label="abc",
...     has_kind=E,
...     label_rev="cba",
... ).matches(e)
True
>>> s = Selector(has_identifier="abc")
>>> f = E(label="def")
>>> list(s.filter([e, f]))
[<E:abc>]
class Registry(_ctx=None, *, uid=<factory>, label=None, tags=<factory>, templ_hash=None, members=<factory>)[source]

Indexed owning collection with selection and chaining.

A Registry is core’s owning boundary for intra-related entities. It is the canonical dereference mechanism for ID-linked graphs:

  • members are indexed by uid: UUID

  • other references should store only UUIDs and dereference through a registry

### Selection

Use find_one / find_all with a Selector for flexible matching. Do not overload get() with fuzzy identifier logic; get() is strictly UUID → entity.

### Layering

chain_find_all is core’s primitive for layered composition:

  • treat multiple registries as a search chain

  • yields matching members across all registries in order

### Persistence

members is declared with Field(exclude=True) so Pydantic model dumps do not automatically include it. unstructure() and structure() handle member payloads explicitly.

Registry.unstructure() includes all members as unstructured constructor-form dicts. Registry.structure() recreates the registry and re-adds structured members.

### Duplicate IDs

add() silently overwrites existing members for duplicate uid keys.

### Dispatch hooks

Pass _ctx to add, get, or remove to allow higher layers to intercept operations.

__setitem__ intentionally raises; callers must use add() so registry-aware binding and dispatch hooks remain consistent.

See also

None, None

Examples

>>> a = Entity(label="abc"); b = Entity(label="def")
>>> r = Registry(); r.add(a); r.add(b)
>>> len(r.members)
2
>>> r.get(a.uid)  # indexed by uid
<Entity:abc>
>>> r.all_labels() == {"abc", "def"}
True
>>> s = Selector.from_identifier("abc")
>>> r.find_one(s)
<Entity:abc>
>>> c = Entity(label="abc")
>>> q = Registry(); q.add(c)
>>> list(Registry.chain_find_all(r, q, selector=s)) == [a, c]
True
>>> data = r.unstructure()
>>> rr = Registry.structure(data)
>>> len(rr.members)
2
>>> r is not rr and r == rr  # compare by value
True
>>> rr.add(Entity()) # compare by value includes members field
>>> rr != r
True
class Singleton(*, label, uid=<factory>, tags=<factory>, templ_hash=None)[source]

Process-local singleton keyed by label per concrete subclass.

Why

Singletons represent immutable concept-level references that should not be duplicated in-memory. They are identified by (class, label) rather than (class, uid).

Key Features

  • each subclass gets an isolated _instances registry via __init_subclass__();

  • duplicate labels are rejected before model construction;

  • un/structuring is reference-only (kind + label), resolving to existing instances.

Notes

  • structure() expects the referenced instance to already exist.

  • use tokens when singleton concepts need graph-local mutable state.

Example

>>> a = Singleton(label="abc"); _ = Singleton(label="def")
>>> Singleton.has_instance("abc")
True
>>> Singleton.get_instance("abc") is a
True
>>> data = a.unstructure()
>>> Singleton.structure(data) is a
True
class InstanceInheritance(*, label, inherit_from=None, uid=<factory>, tags=<factory>, templ_hash=None)[source]

Singleton with creation-time field inheritance from another instance.

inherit_from copies non-identity, non-private fields from the referent at creation time only. Explicit kwargs override inherited values.

Example

>>> class S(InstanceInheritance): value: str = Field()
>>> S(label="foo", value="bar").value
'bar'
>>> S(label="baz", inherit_from="foo").value
'bar'
>>> S(label="foobar", inherit_from="baz").value
'bar'