Wiki & Ontology - PyCharter
Description: Data Contract Management, ETL Pipelines, and Quality Assurance for Python

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`

    311
    312
    313
    314
    315
    316
    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`

    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    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`

    137
    138
    139
    140
    141
    142
    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`

    158
    159
    160
    161
    162
    163
    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`

    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    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`

    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    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`

     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    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`

    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    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`

     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    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

- Wiki & Ontology guide
- Domain Models and Lifecycle
- Errors