Schema: Entity, Identity, Field#

Reference for declaring a factgraph schema with Python classes: the Entity base class, the Identity and Field descriptors, the compile helpers, and the fg.schema register/extend/apply namespace.

All symbols on this page are importable from factgraph.sdk:

from factgraph.sdk import Entity, Field, Identity
from factgraph.sdk import (
    compile_schema_from_classes,
    schema_preflight_from_classes,
    build_authoring_schema_from_classes,
)

A schema is declared by subclassing Entity and assigning annotated Identity and Field descriptors. The EntityMeta metaclass reads the annotations at class-definition time and compiles them into a per-class __sdk_entity_spec__. FactGraph.create(schema_classes=[...]) consumes those specs to build the graph schema.

Entity#

Base class for Python schema entity declarations. Subclass it and declare annotated Identity and Field descriptors.

class Entity(metaclass=EntityMeta)

The metaclass processes every subclass at definition time:

  • It scans the class __annotations__ and matches each annotated name to an assigned Identity or Field descriptor.
  • It requires at least one Identity field. A subclass with no Identity field raises SDKSchemaError: "Entity '<name>' must declare at least one Identity field".
  • It compiles the result into the class attribute __sdk_entity_spec__, a dict with keys entity_type, identity_fields, fields, and optionally version, repr, and tags (the last three come from a nested Meta class, see below).

Annotations without an assigned Identity or Field descriptor are ignored. The class docstring is not schema metadata.

Entity.sdk_entity_spec()#

@classmethod
def sdk_entity_spec(cls) -> dict[str, Any]

Returns the compiled __sdk_entity_spec__ dict.

Returns: dict[str, Any].

Raises: SDKSchemaError if the class is not a compiled Entity declaration ("class '<name>' is not a compiled Entity declaration").

Instantiation#

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

Entity(**kwargs) builds a plain in-memory object. Each keyword must name a declared descriptor; an unknown attribute raises SDKSchemaError ("unknown entity attribute: <key>"). Plain instances are in-memory value holders; they are not ledger-backed. The batch-only methods .set, .add, and .retract are not available on an unset Field read from a plain instance, and calling them raises SDKSchemaError.

Example:

from factgraph.sdk import Entity, Field, Identity

class User(Entity):
    user_id: str = Identity()
    name: str = Field()
    tags: list[str] = Field()

Entity.Meta#

Optional nested class that carries entity-level metadata. Only three keys are supported: version, tags, and repr. Any other key raises SDKSchemaError: "Entity.Meta only supports version, tags, and repr; got unsupported key: <key>". Callable members and dunder members of Meta are ignored.

KeyTypeValidation
versionstrMust be a non-empty string, else SDKSchemaError ("Entity.Meta.version must be non-empty string").
tagslist[str]Must be a list; each element must be a non-empty string, else SDKSchemaError.
reprstrAn explain-layer template for the entity. Validated as a Meta repr template (see repr templates).

Example:

class Person(Entity):
    class Meta:
        repr = "Person %name"
        version = "v1"
        tags = ["pii"]

    name: str = Identity()
    region: str = Field(repr="%ENT has region %FLD")

Identity#

Declares an identity field on an Entity. Identity fields form the entity’s stable, immutable identity bundle and are used to build idref_v1 references. Every Identity() field participates in the complete identity bundle, and callers must supply the complete bundle when creating an entity.

class Identity(_DataMember):
    def __init__(
        self,
        *,
        pattern: str | None = None,
        repr: str | None = None,
        **legacy_kwargs: Any,
    ) -> None

Parameters:

ParameterTypeDefaultDescription
patternstr | NoneNoneKeyword-only. Regular-expression constraint for string identity values. Must be a non-empty string and a valid regular expression. Only supported for string-typed members.
reprstr | NoneNoneKeyword-only. Explain-layer representation template. Validated and compiled into Schema IR without affecting the schema digest. See repr templates.

Cardinality: Identity fields are always single-valued. A non-single annotation (for example list[str]) raises SDKSchemaError: "Identity fields must use a single-value annotation".

Raises:

  • SDKSchemaError if any keyword other than pattern or repr is passed. The message names the unsupported arguments and notes that primary_key, default, and default_factory are not accepted (all Identity fields are immutable anchor members).
  • SDKSchemaError if pattern is not a non-empty string, or is not a valid regular expression.
  • SDKSchemaError if pattern is set on a non-string member ("pattern= is only supported for string-typed Identity/Field members").
  • SDKSchemaError if repr is not a non-empty string, or violates the member repr template rules.

Example:

class TenantUser(Entity):
    user_id: str = Identity()
    tenant_id: str = Identity()
    name: str = Field()

Reading the descriptor from the class (for example User.user_id) returns the Identity descriptor itself, which is used as a navigation key by the fg.fields.* and fg.assertions.* APIs.

Field#

Declares a non-identity field on an Entity.

class Field(_DataMember):
    def __init__(
        self,
        *,
        pattern: str | None = None,
        repr: str | None = None,
        **legacy_kwargs: Any,
    ) -> None

Parameters:

ParameterTypeDefaultDescription
patternstr | NoneNoneKeyword-only. Regular-expression constraint for string fields. Must be a non-empty string and a valid regular expression. Only supported for string-typed fields.
reprstr | NoneNoneKeyword-only. Explain-layer representation template. Validated and compiled into Schema IR without affecting the schema digest. See repr templates.

Cardinality: inferred from the type annotation, not passed as an argument. Scalar annotations create single-value fields; list[T], set[T], frozenset[T], and variadic tuple[T, ...] annotations create multi-value fields. See Cardinality. The Field(cardinality=...) keyword is removed; passing it raises SDKSchemaError with a message directing you to use a scalar annotation (single) or a collection annotation (multi).

Defaults and backfill: fields do not support defaults or backfill. A missing single field reads as None; a missing multi field reads as ().

Raises:

  • SDKSchemaError if any keyword other than pattern or repr is passed (the message documents the cardinality= removal).
  • SDKSchemaError if pattern is not a non-empty string, is not a valid regular expression, or is set on a non-string field.
  • SDKSchemaError if repr is not a non-empty string, or violates the member repr template rules.

Field.cardinality#

@property
def cardinality(self) -> str

Returns the inferred cardinality, "single" or "multi". Available only after the descriptor is bound to a schema class. Reading it before binding raises SDKSchemaError: "Field cardinality is unavailable until the descriptor is bound to a schema class".

Example:

class Person(Entity):
    name: str = Identity()
    region: str = Field()              # single
    tags: list[str] = Field()         # multi
    age: int = Field()                 # single, int-typed

Person.region.cardinality   # "single"
Person.tags.cardinality     # "multi"

Cardinality#

Cardinality is determined entirely by the annotation, evaluated against both real type objects and string (forward-reference) annotations.

Annotation formCardinality
scalar, for example str, int, boolsingle
list[T], set[T], frozenset[T] (also List, Set, FrozenSet)multi
tuple[T, ...] (variadic, also Tuple[T, ...])multi
Literal[...]single (enum)

Constraints:

  • A collection annotation must specify exactly one element type, else SDKSchemaError ("multi-cardinality field annotation must specify exactly one element type").
  • The element type of a collection must itself be scalar (single), else SDKSchemaError ("multi-cardinality fields must use a scalar element annotation").
  • A non-variadic tuple[...] is rejected: SDKSchemaError ("tuple fields must use tuple[T, ...] for multi-cardinality Form I fields").
  • Optional[...] and Union[...] annotations (including X | Y) are rejected: SDKSchemaError ("Optional/Union annotations are not supported in Form I schema declarations").
  • dict[...] annotations are rejected: SDKSchemaError ("dict annotations are not supported in Form I schema declarations").
  • Any other parameterized generic is rejected: SDKSchemaError ("unsupported generic annotation in Form I schema declarations").

Type domains#

Each annotation maps to a canonical type domain. Built-in scalars map as follows:

Python annotationType domain
strstring
intint
boolbool
bytesbytes
floatfloat64
uuid.UUIDuuid
datetime.datetimetime
Entity subclassentity_ref

An annotation that is an Entity subclass (or an unrecognized type, or an unrecognized name in a string annotation) maps to the entity_ref domain. The canonical domain names (entity_ref, string, int, float64, bool, bytes, time, uuid) are also accepted directly when written as string annotations.

Enum fields with Literal#

A Literal[...] annotation declares an enum field. The compiler computes a single canonical type domain from the literal values:

  • bool value to bool, str to string, int to int, float to float64, bytes to bytes, uuid.UUID to uuid, datetime.datetime to time.
  • All literal values must resolve to the same canonical type domain, else SDKSchemaError ("Literal[...] enum values must all use the same canonical type").
  • Literal[...] must declare at least one value, else SDKSchemaError ("Literal[...] enum fields must declare at least one value").
  • Float-typed literals are rejected: SDKSchemaError ("Literal[...] float enum values are not supported; use an unconstrained float field").
  • A literal value of an unsupported type raises SDKSchemaError ("Literal[...] enum value has unsupported type: <type>").

The declared values are carried into the authoring spec as enum_values.

class Ticket(Entity):
    ticket_id: str = Identity()
    status: Literal["open", "closed"] = Field()

repr templates#

repr templates supply explain-layer text. They are validated at class-definition time and compiled into the Schema IR without affecting the schema digest. Template syntax uses %-prefixed placeholders matching %[A-Za-z_][A-Za-z0-9_]*. A % not followed by a letter or underscore is a malformed placeholder and raises an error.

Member templates (Identity(repr=...), Field(repr=...)) may reference only these reserved placeholders:

PlaceholderMeaning
%CLSthe class
%ENTthe entity
%FLDthe current field value

A member template referencing any other placeholder (for example a sibling field name) raises SDKSchemaError ("member repr references unsupported placeholder %<name>; allowed placeholders are %CLS, %ENT, and %FLD").

Meta templates (Entity.Meta.repr) may reference %CLS and any of the entity’s identity-field names. They may not reference %ENT or %FLD (SDKSchemaError: "Meta.repr cannot reference %ENT" / %FLD), and may not reference a non-identity field ("Meta.repr references non-identity placeholder %<name>").

An identity field whose name collides with a reserved placeholder (CLS, ENT, FLD) is rejected when a Meta repr template is used.

Example:

class Person(Entity):
    class Meta:
        repr = "Person %name"          # %CLS + identity field name

    name: str = Identity(repr="%CLS %ENT %FLD")
    region: str = Field(repr="%ENT has region %FLD")

compile_schema_from_classes(classes, *, generated_at=None) -> dict[str, Any]#

Compiles SDK schema classes into canonical schema IR, the exact schema dictionary that backs a FactGraph. Normal graph construction calls it for you.

def compile_schema_from_classes(
    classes: list[type[Any]],
    *,
    generated_at: str | None = None,
) -> dict[str, Any]

Parameters:

ParameterTypeDefaultDescription
classeslist[type[Any]]requiredNon-empty list of Entity subclasses.
generated_atstr | NoneNoneKeyword-only timestamp override for deterministic tests.

Returns: dict[str, Any], the canonical schema IR.

Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.

Example:

from factgraph.sdk import compile_schema_from_classes
schema_ir = compile_schema_from_classes([Person])

schema_preflight_from_classes(classes, *, generated_at=None) -> dict[str, Any]#

Validates SDK schema classes without creating a graph and returns the authoring preflight report produced by the schema compiler.

def schema_preflight_from_classes(
    classes: list[type[Any]],
    *,
    generated_at: str | None = None,
) -> dict[str, Any]

Parameters: same as compile_schema_from_classes.

Returns: dict[str, Any], the schema preflight report.

Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.

build_authoring_schema_from_classes(classes) -> dict[str, Any]#

Lower-level bridge that builds authoring-schema input from SDK schema classes, used before schema compilation. For normal use, pass schema_classes=[...] to FactGraph.create(...) instead.

def build_authoring_schema_from_classes(
    classes: list[type[Any]],
) -> dict[str, Any]

Parameters:

ParameterTypeDefaultDescription
classeslist[type[Any]]requiredNon-empty list of Entity subclasses.

Returns: dict[str, Any], an authoring-schema payload with key entities (a list of per-entity authoring specs).

Raises: SDKSchemaError if classes is not a non-empty list, or if any element is not an Entity subclass.

Example:

authoring = build_authoring_schema_from_classes([User])
authoring["entities"][0]["entity_type"]   # "User"

fg.schema#

Read-only namespace on a FactGraph for additive schema mutation. The namespace itself cannot be reassigned (FrozenSnapshotError). All three mutating methods are unavailable on attached runtimes; calling them there raises SDKStoreError.

Each method returns a SchemaAddResult (frozen dataclass) with these fields:

FieldTypeDescription
old_digeststrSchema digest before the transition.
new_digeststrSchema digest after the transition.
added_entitieslist[str]Newly added entity types.
added_fieldslist[str]Newly added non-identity fields, as "<Entity>.<field>" strings.

A no-op call (one whose compiled schema digest is unchanged with no added entities or fields) returns a SchemaAddResult whose old_digest equals new_digest and whose added_entities and added_fields are empty.

fg.schema.register(entity_cls) -> SchemaAddResult#

def register(self, entity_cls: type[Entity]) -> SchemaAddResult

Registers a new Entity type.

Parameters: entity_cls (type[Entity]), the entity class to register.

Returns: SchemaAddResult whose added_entities lists the new entity type.

Raises:

  • SchemaConflictError (code SCHEMA_CONFLICT) if the entity type is already registered ("entity_type already registered: <name>").
  • SDKStoreError if called on an attached runtime.

Example:

result = fg.schema.register(Account)
result.added_entities   # ["Account"]

fg.schema.extend(entity_cls) -> SchemaAddResult#

def extend(self, entity_cls: type[Entity]) -> SchemaAddResult

Adds non-identity Fields to an already-registered Entity type. The supplied class must keep the existing identity bundle and existing fields unchanged; only new non-identity fields may be added.

Parameters: entity_cls (type[Entity]), the redeclared entity class carrying the additional fields.

Returns: SchemaAddResult whose added_fields lists the new fields as "<Entity>.<field>".

Raises:

  • SchemaNotFoundError (code SCHEMA_NOT_FOUND) if the entity type is not registered ("entity_type not registered: <name>").
  • SchemaNonAdditiveError (code SCHEMA_NON_ADDITIVE) for any non-additive change: adding or demoting/promoting an identity field, removing a field, changing a field’s cardinality, or changing a field’s type. The schema state is left unchanged when the call is rejected.
  • SDKStoreError if called on an attached runtime.

Example:

class User(Entity):
    user_id: str = Identity()
    name: str = Field()
    tags: list[str] = Field()
    nickname: str = Field()        # new field

result = fg.schema.extend(User)
result.added_fields   # ["User.nickname"]

fg.schema.apply(entity_cls) -> SchemaAddResult#

def apply(self, entity_cls: type[Entity]) -> SchemaAddResult

Registers a new entity or extends an existing one. If the entity type is already registered, it routes to extend; otherwise it routes to register.

Parameters: entity_cls (type[Entity]), the entity class to register or extend.

Returns: SchemaAddResult from the chosen route.

Raises: the same errors as register or extend, depending on the route taken.

  • Three-layer API: the fg.entities, fg.fields, and fg.assertions namespaces that consume Identity/Field descriptors as navigation keys.
  • factgraph: FactGraph.create, from_schema_classes, and load_workspace constructors.
  • Errors: SDKSchemaError, SDKStoreError, SchemaConflictError, SchemaNotFoundError, SchemaNonAdditiveError, and FrozenSnapshotError.
comments powered by Disqus