Skip to content

Wiki & Ontology API

The pycharter.wiki module provides types, parsers, and validators for field-level semantic annotations, concept vocabularies, lineage tracking, and collaborative governance.

See Wiki & Ontology guide for the full conceptual overview.

Core ontology models

OntologyArtifact dataclass

OntologyArtifact(
    version: str, fields: dict[str, FieldOntology] = dict()
)

A complete ontology artifact for a data contract.

Attributes:

Name Type Description
version str

Ontology version string

fields dict[str, FieldOntology]

Mapping of field name to its semantic annotation

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "version": self.version,
        "fields": {name: fo.to_dict() for name, fo in self.fields.items()},
    }

FieldOntology dataclass

FieldOntology(
    concept: str,
    broader: str | None = None,
    definition: str | None = None,
    tags: list[str] = list(),
    owner: str | None = None,
    description: str | None = None,
    deprecated: bool = False,
    lineage: Lineage | None = None,
    relationships: list[Relationship] = list(),
)

Semantic annotation for a single field.

Attributes:

Name Type Description
concept str

The semantic concept this field represents

broader str | None

Parent concept in the hierarchy (deprecated -- use ontologies.yaml)

definition str | None

Human-readable definition (deprecated -- use ontologies.yaml)

tags list[str]

Tags for categorization

owner str | None

Team or person owning this field

description str | None

Field-specific business description

deprecated bool

Whether this field mapping is deprecated

lineage Lineage | None

Data lineage information

relationships list[Relationship]

Relationships to other fields

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {
        "concept": self.concept,
    }
    if self.broader is not None:
        result["broader"] = self.broader
    if self.definition is not None:
        result["definition"] = self.definition
    if self.tags:
        result["tags"] = self.tags
    if self.owner is not None:
        result["owner"] = self.owner
    if self.description is not None:
        result["description"] = self.description
    if self.deprecated:
        result["deprecated"] = self.deprecated
    if self.lineage is not None:
        result["lineage"] = self.lineage.to_dict()
    if self.relationships:
        result["relationships"] = [r.to_dict() for r in self.relationships]
    return result

Lineage dataclass

Lineage(
    derived_from: str | None = None,
    source_system: str | None = None,
)

Tracks the origin of a field.

Attributes:

Name Type Description
derived_from str | None

Field this was derived from (if any)

source_system str | None

System that produces this field

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "derived_from": self.derived_from,
        "source_system": self.source_system,
    }

Relationship dataclass

Relationship(target: str, type: RelationshipType)

A relationship between two fields.

Attributes:

Name Type Description
target str

Target field path (e.g., "customers.id")

type RelationshipType

Type of relationship

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    return {
        "target": self.target,
        "type": self.type.value,
    }

ConceptDefinition dataclass

ConceptDefinition(
    id: str,
    label: str,
    definition: str | None = None,
    broader: str | None = None,
    tags: list[str] = list(),
    alt_labels: list[str] = list(),
    notation: str | None = None,
    examples: list[str] = list(),
    exact_match: str | None = None,
    deprecated: bool = False,
    replaced_by: str | None = None,
    node_kind: str | None = None,
)

Full definition of a single concept in the vocabulary.

Mirrors the extended ontologies.yaml schema and can be losslessly converted to/from SKOS JSON-LD.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary (round-trip safe with YAML schema).

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary (round-trip safe with YAML schema)."""
    result: dict[str, Any] = {"id": self.id, "label": self.label}
    if self.definition is not None:
        result["definition"] = self.definition
    if self.broader is not None:
        result["broader"] = self.broader
    if self.tags:
        result["tags"] = self.tags
    if self.alt_labels:
        result["alt_labels"] = self.alt_labels
    if self.notation is not None:
        result["notation"] = self.notation
    if self.examples:
        result["examples"] = self.examples
    if self.exact_match is not None:
        result["exact_match"] = self.exact_match
    if self.deprecated:
        result["deprecated"] = self.deprecated
    if self.replaced_by is not None:
        result["replaced_by"] = self.replaced_by
    if self.node_kind is not None:
        result["node_kind"] = self.node_kind
    return result

ConceptScheme dataclass

ConceptScheme(
    id: str,
    title: str,
    version: str,
    description: str = "",
    base_uri: str | None = None,
    concepts: list[ConceptDefinition] = list(),
)

A complete concept scheme (vocabulary).

Mirrors the top-level scheme: + concepts: structure in ontologies.yaml.

to_dict

to_dict() -> dict[str, Any]

Convert to dictionary.

Source code in src/pycharter/wiki/ontology/models.py
def to_dict(self) -> dict[str, Any]:
    """Convert to dictionary."""
    result: dict[str, Any] = {
        "scheme": {
            "id": self.id,
            "title": self.title,
            "version": self.version,
        },
        "concepts": [c.to_dict() for c in self.concepts],
    }
    if self.description:
        result["scheme"]["description"] = self.description
    if self.base_uri:
        result["scheme"]["base_uri"] = self.base_uri
    return result

Enumerations

ConceptTier

Bases: str, Enum

Governance tier for concepts.

CORE class-attribute instance-attribute

CORE = 'core'

Tier 1: Core concepts governed by central team.

DOMAIN class-attribute instance-attribute

DOMAIN = 'domain'

Tier 2: Domain concepts governed by domain teams.

FREE class-attribute instance-attribute

FREE = 'free'

Tier 3: Free-form concepts with no governance.

NodeKind

Bases: str, Enum

Classification of concept nodes in the knowledge graph.

ENTITY class-attribute instance-attribute

ENTITY = 'entity'

A thing with identity (customer, product, system).

PROCESS class-attribute instance-attribute

PROCESS = 'process'

A sequence of steps (checkout, onboarding).

EVENT class-attribute instance-attribute

EVENT = 'event'

Something that happens at a point in time.

ATTRIBUTE class-attribute instance-attribute

ATTRIBUTE = 'attribute'

A property or measurement.

RULE class-attribute instance-attribute

RULE = 'rule'

A constraint or policy.

SYSTEM class-attribute instance-attribute

SYSTEM = 'system'

An external system or service.

CONCEPT class-attribute instance-attribute

CONCEPT = 'concept'

An abstract idea.

METRIC class-attribute instance-attribute

METRIC = 'metric'

A computed business measure.

RelationshipType

Bases: str, Enum

Types of relationships between fields or concepts.

REFERENCES class-attribute instance-attribute

REFERENCES = 'references'

Foreign key or reference relationship.

DERIVED_FROM class-attribute instance-attribute

DERIVED_FROM = 'derived_from'

Field is computed from another field.

SAME_AS class-attribute instance-attribute

SAME_AS = 'same_as'

Fields represent the same concept.

RELATED_TO class-attribute instance-attribute

RELATED_TO = 'related_to'

General relationship.

IS_A class-attribute instance-attribute

IS_A = 'is_a'

X is a kind of Y (taxonomy).

HAS class-attribute instance-attribute

HAS = 'has'

X contains or owns Y (composition).

DEPENDS_ON class-attribute instance-attribute

DEPENDS_ON = 'depends_on'

X requires Y to function.

PRECEDES class-attribute instance-attribute

PRECEDES = 'precedes'

X happens before Y (temporal ordering).

CAUSES class-attribute instance-attribute

CAUSES = 'causes'

X leads to or triggers Y.

IMPLEMENTS class-attribute instance-attribute

IMPLEMENTS = 'implements'

X realizes or is a concrete form of Y.

GOVERNS class-attribute instance-attribute

GOVERNS = 'governs'

X constrains or regulates Y.

DESCRIBES class-attribute instance-attribute

DESCRIBES = 'describes'

X is a property or attribute of Y.

EQUIVALENT_TO class-attribute instance-attribute

EQUIVALENT_TO = 'equivalent_to'

X represents the same thing as Y.

Parsing functions

parse_ontology

parse_ontology(
    ontology_data: dict[str, Any],
) -> OntologyArtifact

Parse an ontology dictionary into an OntologyArtifact.

Expected structure: { "version": "1.0.0", "fields": { "customer_id": { "concept": "Customer Identity", "broader": "Entity", ... } } }

Parameters:

Name Type Description Default
ontology_data dict[str, Any]

Ontology data as dictionary

required

Returns:

Type Description
OntologyArtifact

OntologyArtifact with parsed fields

Raises:

Type Description
OntologyError

If the data is not a valid dictionary

Source code in src/pycharter/wiki/ontology/parser.py
def parse_ontology(ontology_data: dict[str, Any]) -> OntologyArtifact:
    """
    Parse an ontology dictionary into an OntologyArtifact.

    Expected structure:
    {
        "version": "1.0.0",
        "fields": {
            "customer_id": {
                "concept": "Customer Identity",
                "broader": "Entity",
                ...
            }
        }
    }

    Args:
        ontology_data: Ontology data as dictionary

    Returns:
        OntologyArtifact with parsed fields

    Raises:
        OntologyError: If the data is not a valid dictionary
    """
    if not isinstance(ontology_data, dict):
        raise OntologyError(
            f"Ontology data must be a dictionary, got {type(ontology_data).__name__}"
        )

    version = ontology_data.get("version", "0.0.0")
    raw_fields = ontology_data.get("fields", {})

    if not isinstance(raw_fields, dict):
        raise OntologyError(
            f"Ontology 'fields' must be a dictionary, got {type(raw_fields).__name__}"
        )

    fields = {}
    for field_name, field_data in raw_fields.items():
        if not isinstance(field_data, dict):
            raise OntologyError(
                f"Field '{field_name}' must be a dictionary, "
                f"got {type(field_data).__name__}"
            )
        fields[field_name] = _parse_field_ontology(field_data)

    return OntologyArtifact(version=version, fields=fields)

parse_ontology_file

parse_ontology_file(file_path: str) -> OntologyArtifact

Load and parse an ontology file (YAML or JSON).

Parameters:

Name Type Description Default
file_path str

Path to ontology file

required

Returns:

Type Description
OntologyArtifact

OntologyArtifact with parsed fields

Raises:

Type Description
OntologyError

If the file cannot be read or parsed

Source code in src/pycharter/wiki/ontology/parser.py
def parse_ontology_file(file_path: str) -> OntologyArtifact:
    """
    Load and parse an ontology file (YAML or JSON).

    Args:
        file_path: Path to ontology file

    Returns:
        OntologyArtifact with parsed fields

    Raises:
        OntologyError: If the file cannot be read or parsed
    """
    path = Path(file_path)

    if not path.exists():
        raise OntologyError(f"Ontology file not found: {file_path}", path=file_path)

    suffix = path.suffix.lower()

    if suffix in (".jsonld", ".json"):
        try:
            from pycharter.wiki.rdf.jsonld_parser import parse_ontology_jsonld
        except ImportError as exc:
            raise OntologyError(
                "JSON-LD support requires the 'ontology' extra: "
                "pip install pycharter[ontology]",
                path=file_path,
            ) from exc
        import json as _json

        try:
            with open(path, "r", encoding="utf-8") as f:
                data = _json.load(f)
        except _json.JSONDecodeError as e:
            raise OntologyError(f"Invalid JSON: {e}", path=file_path) from e
        return parse_ontology_jsonld(data)

    if suffix not in (".yaml", ".yml"):
        raise OntologyError(
            f"Unsupported file format: {suffix}. Supported: .yaml, .yml, .jsonld, .json",
            path=file_path,
        )

    try:
        with open(path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
    except yaml.YAMLError as e:
        raise OntologyError(f"Invalid YAML: {e}", path=file_path) from e

    if not isinstance(data, dict):
        raise OntologyError(
            f"Ontology file must contain a dictionary, got {type(data).__name__}",
            path=file_path,
        )

    return parse_ontology(data)

Validation

validate_ontology

validate_ontology(
    ontology_data: dict[str, Any],
) -> list[dict[str, str]]

Validate raw ontology data before parsing.

Checks structural requirements: - Must have 'version' field - Must have 'fields' dict - Each field must have 'concept' - Relationship types must be valid - No circular broader references

Parameters:

Name Type Description Default
ontology_data dict[str, Any]

Raw ontology dictionary

required

Returns:

Type Description
list[dict[str, str]]

List of error dicts with 'path' and 'message' keys.

list[dict[str, str]]

Empty list means valid.

Source code in src/pycharter/wiki/ontology/validator.py
def validate_ontology(ontology_data: dict[str, Any]) -> list[dict[str, str]]:
    """
    Validate raw ontology data before parsing.

    Checks structural requirements:
    - Must have 'version' field
    - Must have 'fields' dict
    - Each field must have 'concept'
    - Relationship types must be valid
    - No circular broader references

    Args:
        ontology_data: Raw ontology dictionary

    Returns:
        List of error dicts with 'path' and 'message' keys.
        Empty list means valid.
    """
    errors: list[dict[str, str]] = []

    if not isinstance(ontology_data, dict):
        errors.append({"path": "", "message": "Ontology must be a dictionary"})
        return errors

    # Check version
    if "version" not in ontology_data:
        errors.append(
            {"path": "version", "message": "Missing required field 'version'"}
        )

    # Check fields
    fields = ontology_data.get("fields")
    if fields is None:
        errors.append({"path": "fields", "message": "Missing required field 'fields'"})
        return errors

    if not isinstance(fields, dict):
        errors.append({"path": "fields", "message": "'fields' must be a dictionary"})
        return errors

    valid_rel_types = {t.value for t in RelationshipType}

    for field_name, field_data in fields.items():
        prefix = f"fields.{field_name}"

        if not isinstance(field_data, dict):
            errors.append(
                {
                    "path": prefix,
                    "message": f"Field '{field_name}' must be a dictionary",
                }
            )
            continue

        # concept is required
        if "concept" not in field_data:
            errors.append(
                {
                    "path": f"{prefix}.concept",
                    "message": "Missing required field 'concept'",
                }
            )

        # tags must be a list
        tags = field_data.get("tags")
        if tags is not None and not isinstance(tags, list):
            errors.append(
                {"path": f"{prefix}.tags", "message": "'tags' must be a list"}
            )

        # lineage must be a dict
        lineage = field_data.get("lineage")
        if lineage is not None and not isinstance(lineage, dict):
            errors.append(
                {
                    "path": f"{prefix}.lineage",
                    "message": "'lineage' must be a dictionary",
                }
            )

        # relationships must be a list of dicts with valid types
        relationships = field_data.get("relationships")
        if relationships is not None:
            if not isinstance(relationships, list):
                errors.append(
                    {
                        "path": f"{prefix}.relationships",
                        "message": "'relationships' must be a list",
                    }
                )
            else:
                for i, rel in enumerate(relationships):
                    rel_prefix = f"{prefix}.relationships[{i}]"
                    if not isinstance(rel, dict):
                        errors.append(
                            {
                                "path": rel_prefix,
                                "message": "Relationship must be a dictionary",
                            }
                        )
                        continue
                    if "target" not in rel:
                        errors.append(
                            {
                                "path": f"{rel_prefix}.target",
                                "message": "Missing 'target'",
                            }
                        )
                    rel_type = rel.get("type")
                    if rel_type is not None and rel_type not in valid_rel_types:
                        errors.append(
                            {
                                "path": f"{rel_prefix}.type",
                                "message": f"Invalid relationship type '{rel_type}'. "
                                f"Valid: {sorted(valid_rel_types)}",
                            }
                        )

    # Check for circular broader references
    if isinstance(fields, dict):
        errors.extend(_check_circular_broader(fields))

    return errors

Exceptions

PyCharterWikiError

Bases: Exception

Base exception for all pycharter-wiki errors.

Catch this to handle any pycharter-wiki-related failure without depending on specific exception types. Subclasses provide more specific handling.

Example

try: artifact = parse_ontology_file("ontology.yaml") except PyCharterWikiError as e: logger.error("Ontology operation failed: %s", e)

Import

# Top-level convenience imports
from pycharter.wiki import (
    OntologyArtifact,
    FieldOntology,
    Lineage,
    Relationship,
    ConceptTier,
    RelationshipType,
    parse_ontology,
    parse_ontology_file,
    validate_ontology,
    PyCharterWikiError,
)

# Extended models
from pycharter.wiki.ontology.models import (
    ConceptDefinition,
    ConceptScheme,
    NodeKind,
)

Relationship inverse map

Each RelationshipType has a defined inverse, accessible via:

from pycharter.wiki.ontology.models import resolve_inverse

print(resolve_inverse("is_a"))        # "has_subtype"
print(resolve_inverse("derived_from")) # "derives"
print(resolve_inverse("precedes"))     # "follows"

See also