From 56ee902c207da7bd4022c84131f01730412eaca3 Mon Sep 17 00:00:00 2001 From: Andrew Nepogoda Date: Tue, 27 Oct 2026 13:41:07 +0100 Subject: [PATCH 1/5] feat(python): add ontology models to the shared SDK Add Pydantic models mirroring ontology/ontology.json so converters can share them instead of keeping their own DTOs (ossie_ontology.spec in apache/ossie-converters): - OssieOntologyDocument, OssieConcept, OssieOntologyRelationship, OssieRole, OssieConceptType, OssieMultiplicity - OssieConceptMapping, OssieObjectMapping, OssieReferentMapping, OssieLinkMapping and the deprecated embedded OssieOntologyMapping - OssieVersion, shared with OssieDocument The models are frozen and forbid unknown keys like the existing ones, and follow the schema's required fields and anyOf rules. OssieDocument is unchanged apart from typing its version as OssieVersion. Tests check each model against its schema node. --- python/README.md | 4 + python/src/ossie/__init__.py | 24 +++ python/src/ossie/models.py | 202 +++++++++++++++++++- python/tests/test_ontology_models.py | 263 +++++++++++++++++++++++++++ 4 files changed, 485 insertions(+), 8 deletions(-) create mode 100644 python/tests/test_ontology_models.py diff --git a/python/README.md b/python/README.md index 504ec7944..7d87f5e81 100644 --- a/python/README.md +++ b/python/README.md @@ -38,6 +38,10 @@ including keys in nested objects, so an earlier value is not silently lost. The YAML loader preserves aliases, repeated merge directives and explicit overrides of merged keys. +`OssieOntologyDocument` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), +the relationships among them (`OssieOntologyRelationship`, distinct from the dataset +join `OssieRelationship`), and, deprecated, embedded `ontology_mappings`. + ## Development ### Prerequisites diff --git a/python/src/ossie/__init__.py b/python/src/ossie/__init__.py index d9c48762d..dddc37c02 100644 --- a/python/src/ossie/__init__.py +++ b/python/src/ossie/__init__.py @@ -25,6 +25,9 @@ from ossie.models import ( OssieAIContext, OssieAIContextObject, + OssieConcept, + OssieConceptMapping, + OssieConceptType, OssieCustomExtension, OssieDataset, OssieDataType, @@ -34,15 +37,27 @@ OssieDocument, OssieExpression, OssieField, + OssieLinkMapping, OssieMetric, + OssieMultiplicity, + OssieObjectMapping, + OssieOntologyDocument, + OssieOntologyMapping, + OssieOntologyRelationship, + OssieReferentMapping, OssieRelationship, + OssieRole, OssieSemanticModel, OssieVendor, + OssieVersion, ) __all__ = [ "OssieAIContext", "OssieAIContextObject", + "OssieConcept", + "OssieConceptMapping", + "OssieConceptType", "OssieCustomExtension", "OssieDataset", "OssieDataType", @@ -52,8 +67,17 @@ "OssieDocument", "OssieExpression", "OssieField", + "OssieLinkMapping", "OssieMetric", + "OssieMultiplicity", + "OssieObjectMapping", + "OssieOntologyDocument", + "OssieOntologyMapping", + "OssieOntologyRelationship", + "OssieReferentMapping", "OssieRelationship", + "OssieRole", "OssieSemanticModel", "OssieVendor", + "OssieVersion", ] diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index a7bc947b6..f36707764 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations + import json import math from enum import Enum @@ -234,7 +236,7 @@ class OssieExpression(BaseModel): dialects: Optional[list[OssieDialectExpression]] = Field(default=None, min_length=1) @model_validator(mode="after") - def _require_sql_or_dialects(self) -> "OssieExpression": + def _require_sql_or_dialects(self) -> OssieExpression: if self.sql is None and not self.dialects: raise ValueError("expression requires at least one of 'sql' or 'dialects'") return self @@ -343,15 +345,19 @@ class OssieSemanticModel(BaseModel): custom_extensions: Optional[list[OssieCustomExtension]] = None +# The specification version every Ossie document declares. Each schema pins it with +# `const`, so a document may not declare any other version. Mirrored as a Literal +# rather than a plain str, which would let the package serialize a version the +# schema rejects. +OssieVersion = Literal["0.2.0.dev0"] + + class OssieDocument(OssieSemanticModel): """A single semantic model with document metadata at the root.""" model_config = ConfigDict(frozen=True, extra="forbid") - # The schema pins this with `const`, so a document may not declare any other - # version. Mirrored as a Literal rather than a plain str, which would let the - # package serialize a version the schema rejects. - version: Literal["0.2.0.dev0"] = "0.2.0.dev0" + version: OssieVersion = "0.2.0.dev0" @model_serializer(mode="wrap") def _serialize_document(self, handler: SerializerFunctionWrapHandler): @@ -384,7 +390,7 @@ def to_ossie_json(self, **kwargs: Any) -> str: return self.model_dump_json(**kwargs) @classmethod - def from_ossie_yaml(cls, text: str) -> "OssieDocument": + def from_ossie_yaml(cls, text: str) -> OssieDocument: """Parse an Ossie YAML document, checking what pydantic alone cannot. The counterpart of :meth:`to_ossie_yaml`. ``model_validate`` stays lenient, @@ -411,7 +417,7 @@ def from_ossie_yaml(cls, text: str) -> "OssieDocument": return cls._from_document(yaml.load(text, Loader=UniqueKeyLoader)) @classmethod - def from_ossie_json(cls, text: str) -> "OssieDocument": + def from_ossie_json(cls, text: str) -> OssieDocument: """Parse an Ossie JSON document, checking what pydantic alone cannot. The counterpart of :meth:`to_ossie_json`; see :meth:`from_ossie_yaml` for what @@ -420,7 +426,7 @@ def from_ossie_json(cls, text: str) -> "OssieDocument": return cls._from_document(json.loads(text, object_pairs_hook=unique_json_object)) @classmethod - def _from_document(cls, data: Any) -> "OssieDocument": + def _from_document(cls, data: Any) -> OssieDocument: for reason in (_NULL, _NON_FINITE): paths = [path for path, why in _invalid_properties(data) if why == reason] if paths: @@ -434,3 +440,183 @@ def _from_document(cls, data: Any) -> "OssieDocument": # the schema lists it as required, so a document without one is invalid. raise ValueError("document is missing the required property: version") return cls.model_validate(data) + + +def _require_any(model: BaseModel, *names: str) -> None: + """Mirror a schema `anyOf` of `required` lists: at least one of `names` is set.""" + if all(getattr(model, name) is None for name in names): + listed = " or ".join(f"'{name}'" for name in names) + raise ValueError(f"{type(model).__name__} requires at least one of {listed}") + + +# --------------------------------------------------------------------------- +# Ontology (ontology/ontology.json) +# --------------------------------------------------------------------------- + + +class OssieConceptType(str, Enum): + """Whether a concept is an entity type or a value type.""" + + ENTITY_TYPE = "EntityType" + VALUE_TYPE = "ValueType" + + +class OssieMultiplicity(str, Enum): + """Multiplicity constraint on the last role of an ontology relationship.""" + + MANY_TO_ONE = "ManyToOne" + ONE_TO_ONE = "OneToOne" + + +class OssieRole(BaseModel): + """An additional role in an ontology relationship. + + The first role is implicit: it is played by the concept the relationship is + declared under. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + concept: str + # Needed only when the same concept plays more than one role in the relationship. + name: Optional[str] = None + + +class OssieOntologyRelationship(BaseModel): + """Relationship between concepts, declared under the concept playing its first role. + + Not to be confused with `OssieRelationship`, the join between two datasets of a + semantic model. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: str + description: Optional[str] = None + roles: Optional[list[OssieRole]] = None + multiplicity: Optional[OssieMultiplicity] = None + derived_by: Optional[list[str]] = None + requires: Optional[list[str]] = None + verbalizes: list[str] + iri: Optional[str] = None + + +class OssieConcept(BaseModel): + """A concept and the relationships where it plays the first role. + + The schema calls this node `OntologyComponent`. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + concept: str + type: OssieConceptType + description: Optional[str] = None + extends: Optional[list[str]] = None + derived_by: Optional[list[str]] = None + identify_by: Optional[list[str]] = None + requires: Optional[list[str]] = None + relationships: Optional[list[OssieOntologyRelationship]] = None + iri: Optional[str] = None + + +# --------------------------------------------------------------------------- +# Ontology mappings (ontology/ontology.json) +# --------------------------------------------------------------------------- + + +class OssieReferentMapping(BaseModel): + """Maps logical constructs to an identifying relationship of an entity type. + + A leaf carries an `expression`; a referent whose player is itself an entity + nests further `referent_mappings` instead. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + relationship: str + expression: Optional[str] = None + referent_mappings: Optional[list[OssieReferentMapping]] = Field(default=None, min_length=1) + + @model_validator(mode="after") + def _require_expression_or_referents(self) -> OssieReferentMapping: + _require_any(self, "expression", "referent_mappings") + return self + + +class OssieObjectMapping(BaseModel): + """Identifies objects of a concept from the values of one or more fields.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + concept: Optional[str] = None + referent_mappings: Optional[list[OssieReferentMapping]] = Field(default=None, min_length=1) + expression: Optional[str] = None + + @model_validator(mode="after") + def _require_expression_or_referents(self) -> OssieObjectMapping: + _require_any(self, "expression", "referent_mappings") + return self + + +class OssieLinkMapping(BaseModel): + """A node in a tree of mappings to the links of ontology relationships. + + A node's depth equals the arity of the relationship it names; each child extends + the tuple mapped by its parent by one role. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + relationship: Optional[str] = None + object_mapping: OssieObjectMapping + children: Optional[list[OssieLinkMapping]] = None + + +class OssieConceptMapping(BaseModel): + """Mappings that populate one concept and the relationships grouped under it.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + concept: str + object_mappings: Optional[list[OssieObjectMapping]] = Field(default=None, min_length=1) + link_mappings: Optional[list[OssieLinkMapping]] = Field(default=None, min_length=1) + + @model_validator(mode="after") + def _require_object_or_link_mappings(self) -> OssieConceptMapping: + _require_any(self, "object_mappings", "link_mappings") + return self + + +class OssieOntologyMapping(BaseModel): + """A semantic model embedded in an ontology, with the mappings onto that ontology. + + Deprecated by the schema, which accepts it only so existing documents continue + to validate. New mappings are standalone documents validated against + `ontology/mapping.json`. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: Optional[str] = None + description: Optional[str] = None + # The schema embeds the whole core document here, `version` included. + semantic_model: OssieDocument + concept_mappings: list[OssieConceptMapping] + + +class OssieOntologyDocument(BaseModel): + """An ontology: concepts, the relationships among them, and their rules.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + version: OssieVersion = "0.2.0.dev0" + name: str + description: Optional[str] = None + ai_context: Optional[OssieAIContext] = None + requires: Optional[list[str]] = None + ontology: list[OssieConcept] = Field(..., min_length=1) + # Deprecated by the schema; see `OssieOntologyMapping`. + ontology_mappings: Optional[list[OssieOntologyMapping]] = None + # Namespace prefix -> the IRI it abbreviates, for `iri` values written as QNames. + prefixes: Optional[dict[str, str]] = None diff --git a/python/tests/test_ontology_models.py b/python/tests/test_ontology_models.py new file mode 100644 index 000000000..4e1c6cdfd --- /dev/null +++ b/python/tests/test_ontology_models.py @@ -0,0 +1,263 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json +from pathlib import Path +from typing import get_args + +import pytest +import yaml +from pydantic import ValidationError + +from ossie import ( + OssieConcept, + OssieConceptMapping, + OssieConceptType, + OssieDocument, + OssieLinkMapping, + OssieMultiplicity, + OssieObjectMapping, + OssieOntologyDocument, + OssieOntologyMapping, + OssieOntologyRelationship, + OssieReferentMapping, + OssieRole, + OssieVersion, +) + +_REPO = Path(__file__).parents[2] + + +def _schema(name: str) -> dict: + return json.loads((_REPO / "ontology" / name).read_text()) + + +@pytest.fixture +def ontology_data(document_data: dict) -> dict: + return { + "version": "0.2.0.dev0", + "name": "events", + "ontology": [ + { + "concept": "Event", + "type": "EntityType", + "identify_by": ["at"], + "relationships": [ + { + "name": "at", + "roles": [{"concept": "DateTime"}], + "multiplicity": "OneToOne", + "verbalizes": ["{Event} occurred at {DateTime}"], + } + ], + } + ], + "ontology_mappings": [ + { + "name": "events_mapping", + "semantic_model": document_data, + "concept_mappings": [ + { + "concept": "Event", + "object_mappings": [ + { + "referent_mappings": [ + {"relationship": "at", "expression": "events.occurred_at"} + ] + } + ], + } + ], + } + ], + } + + +# --------------------------------------------------------------------------- +# Examples +# --------------------------------------------------------------------------- + + +def _dump(document: OssieOntologyDocument) -> dict: + return document.model_dump(mode="json", by_alias=True, exclude_none=True) + + +def test_example_round_trips() -> None: + data = yaml.safe_load((_REPO / "examples" / "flights.ontology.yaml").read_text()) + + assert _dump(OssieOntologyDocument.model_validate(data)) == data + + +def test_embedded_mapping_carries_a_full_semantic_model_document(ontology_data: dict) -> None: + ontology = OssieOntologyDocument.model_validate(ontology_data) + + semantic_model = ontology.ontology_mappings[0].semantic_model + assert isinstance(semantic_model, OssieDocument) + assert semantic_model.version == "0.2.0.dev0" + assert OssieOntologyDocument.model_validate(_dump(ontology)) == ontology + + +def test_serialization_omits_absent_fields_and_puts_version_first(ontology_data: dict) -> None: + del ontology_data["ontology_mappings"] + ontology = OssieOntologyDocument.model_validate(ontology_data) + + assert _dump(ontology) == ontology_data + assert list(_dump(ontology))[0] == "version" + + +# --------------------------------------------------------------------------- +# Schema parity +# --------------------------------------------------------------------------- + + +def test_enums_match_the_ontology_schema() -> None: + defs = _schema("ontology.json")["$defs"] + + assert [member.value for member in OssieConceptType] == defs["ConceptType"]["enum"] + assert [member.value for member in OssieMultiplicity] == defs["Multiplicity"]["enum"] + + +def test_version_matches_the_schema_const() -> None: + const = _schema("ontology.json")["properties"]["version"]["const"] + + assert get_args(OssieVersion) == (const,) + + +def test_models_mirror_the_schema_properties() -> None: + """Each object node maps to a model with the same properties, required set and strictness. + + The root node is keyed by "". Comparing the property names catches a field added to + the schema but not the model, and vice versa. + """ + models = { + "": OssieOntologyDocument, + "OntologyComponent": OssieConcept, + "Relationship": OssieOntologyRelationship, + "Role": OssieRole, + "OntologyMap": OssieOntologyMapping, + "ConceptMapping": OssieConceptMapping, + "ObjectMapping": OssieObjectMapping, + "ReferentMapping": OssieReferentMapping, + "LinkMapping": OssieLinkMapping, + } + schema = _schema("ontology.json") + nodes = {"": schema} | { + key: node for key, node in schema["$defs"].items() if node.get("type") == "object" + } + assert set(nodes) == set(models) + + for key, model in models.items(): + node = nodes[key] + fields = {field.alias or name: field for name, field in model.model_fields.items()} + assert set(fields) == set(node["properties"]), model.__name__ + # `version` carries a default for Python construction; documents still require it. + required = {name for name, field in fields.items() if field.is_required()} + assert required | ({"version"} & set(fields)) == set(node.get("required", [])), ( + model.__name__ + ) + assert node["additionalProperties"] is False + assert model.model_config.get("extra") == "forbid", model.__name__ + + +# --------------------------------------------------------------------------- +# Constraints the field types cannot express +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("model", "data"), + [ + (OssieReferentMapping, {"relationship": "at"}), + (OssieObjectMapping, {"concept": "Event"}), + (OssieConceptMapping, {"concept": "Event"}), + ], +) +def test_any_of_requires_one_alternative(model: type, data: dict) -> None: + with pytest.raises(ValidationError, match="requires at least one of"): + model.model_validate(data) + + +@pytest.mark.parametrize( + "data", + [ + {"concept": "Event", "object_mappings": []}, + {"concept": "Event", "link_mappings": []}, + ], +) +def test_concept_mapping_lists_cannot_be_empty(data: dict) -> None: + with pytest.raises(ValidationError): + OssieConceptMapping.model_validate(data) + + +def test_referent_mappings_nest() -> None: + nested = OssieReferentMapping.model_validate( + { + "relationship": "order", + "referent_mappings": [{"relationship": "nr", "expression": "LINES.order_nr"}], + } + ) + + assert nested.referent_mappings[0].expression == "LINES.order_nr" + + +def test_link_mappings_nest() -> None: + mapping = OssieLinkMapping.model_validate( + { + "object_mapping": {"expression": "ITEMS.sku"}, + "relationship": "active", + "children": [ + {"object_mapping": {"concept": "Store", "expression": "ITEMS.store"}} + ], + } + ) + + assert mapping.children[0].object_mapping.concept == "Store" + + +@pytest.mark.parametrize( + ("path", "key"), + [ + (("ontology", 0), "type"), + (("ontology", 0, "relationships", 0), "verbalizes"), + (("ontology", 0, "relationships", 0, "roles", 0), "concept"), + (("ontology_mappings", 0), "semantic_model"), + ], +) +def test_required_properties(ontology_data: dict, path: tuple, key: str) -> None: + container = ontology_data + for part in path: + container = container[part] + del container[key] + + with pytest.raises(ValidationError) as error: + OssieOntologyDocument.model_validate(ontology_data) + + assert any(item["loc"] == (*path, key) for item in error.value.errors()) + + +def test_ontology_requires_a_concept(ontology_data: dict) -> None: + ontology_data["ontology"] = [] + + with pytest.raises(ValidationError): + OssieOntologyDocument.model_validate(ontology_data) + + +def test_invalid_multiplicity_is_rejected(ontology_data: dict) -> None: + ontology_data["ontology"][0]["relationships"][0]["multiplicity"] = "ManyToMany" + + with pytest.raises(ValidationError): + OssieOntologyDocument.model_validate(ontology_data) From 0d039bd56086710ffeacb7f1ef1495e1b5b4738c Mon Sep 17 00:00:00 2001 From: Andrew Nepogoda Date: Wed, 28 Oct 2026 13:20:55 +0100 Subject: [PATCH 2/5] refactor(python): share one document base across semantic models and ontologies Make OssieDocument the base of every Ossie file root. It holds the version, version-first serialization and the to_ossie_* / from_ossie_* methods with their document checks (duplicate keys, nulls, non-finite numbers, cyclic aliases, missing version). - OssieSemanticModel extends OssieDocument and takes over the role of the old OssieDocument: it now carries version and the I/O methods. - OssieOntologyDocument is renamed OssieOntology and extends OssieDocument, so ontology files load with the same checks. - OssieOntologyMapping.semantic_model is typed OssieSemanticModel. This is a breaking change for code that builds or loads OssieDocument as a semantic model; it switches to OssieSemanticModel. --- python/README.md | 18 +++-- python/src/ossie/__init__.py | 4 +- python/src/ossie/models.py | 52 ++++++------ python/tests/test_ai_context_serialization.py | 14 ++-- python/tests/test_loading.py | 26 +++--- python/tests/test_models.py | 69 ++++++---------- python/tests/test_ontology_models.py | 54 ++++++++++--- python/tests/test_serialization.py | 80 +++++++++---------- python/tests/test_validation.py | 3 +- 9 files changed, 165 insertions(+), 155 deletions(-) diff --git a/python/README.md b/python/README.md index 7d87f5e81..3e45ef051 100644 --- a/python/README.md +++ b/python/README.md @@ -21,27 +21,33 @@ The Apache Ossie Python package provides Pydantic v2 models for the Apache Ossie semantic model specification. It is the shared foundation used by Apache Ossie converters to parse, construct, validate, and serialize Ossie documents from Python application. -Each `OssieDocument` is one semantic model: `name`, `datasets`, `relationships`, +Each `OssieSemanticModel` is one semantic model: `name`, `datasets`, `relationships`, and `metrics` sit at the root alongside `version`. -Construct documents with `OssieDocument(name="sales", datasets=[...])` and access +Construct documents with `OssieSemanticModel(name="sales", datasets=[...])` and access their datasets as `document.datasets`. JSON and YAML serialization use the same flat shape, with `version` serialized first when included. The former `semantic_model` wrapper is rejected. Unwrap old single-model documents and split multi-model documents into separate files, preserving model contents and setting `version` in each file before loading them. -Use `OssieDocument` for models embedded in ontology maps. +The same `OssieSemanticModel`, `version` included, is embedded in ontology maps. +Earlier releases used `OssieDocument` for semantic models; that role now belongs to +`OssieSemanticModel`, so replace `OssieDocument(...)` and +`OssieDocument.from_ossie_yaml(...)` with their `OssieSemanticModel` equivalents. -Load serialized documents with `OssieDocument.from_ossie_yaml(text)` or -`OssieDocument.from_ossie_json(text)`. Both reject duplicate mapping keys, +Load serialized documents with `OssieSemanticModel.from_ossie_yaml(text)` or +`OssieSemanticModel.from_ossie_json(text)`. Both reject duplicate mapping keys, including keys in nested objects, so an earlier value is not silently lost. The YAML loader preserves aliases, repeated merge directives and explicit overrides of merged keys. -`OssieOntologyDocument` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), +`OssieOntology` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), the relationships among them (`OssieOntologyRelationship`, distinct from the dataset join `OssieRelationship`), and, deprecated, embedded `ontology_mappings`. +`OssieSemanticModel` and `OssieOntology` both extend `OssieDocument`, which holds `version` and the `to_ossie_*` and `from_ossie_*` methods. An ontology +file therefore loads with `OssieOntology.from_ossie_yaml(text)`, under the same checks. + ## Development ### Prerequisites diff --git a/python/src/ossie/__init__.py b/python/src/ossie/__init__.py index dddc37c02..534d7c3c6 100644 --- a/python/src/ossie/__init__.py +++ b/python/src/ossie/__init__.py @@ -41,7 +41,7 @@ OssieMetric, OssieMultiplicity, OssieObjectMapping, - OssieOntologyDocument, + OssieOntology, OssieOntologyMapping, OssieOntologyRelationship, OssieReferentMapping, @@ -71,7 +71,7 @@ "OssieMetric", "OssieMultiplicity", "OssieObjectMapping", - "OssieOntologyDocument", + "OssieOntology", "OssieOntologyMapping", "OssieOntologyRelationship", "OssieReferentMapping", diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index f36707764..ed4277f42 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -20,7 +20,7 @@ import json import math from enum import Enum -from typing import Annotated, Any, Literal, Optional, Union +from typing import Annotated, Any, Literal, Optional, Self, Union import yaml from pydantic import ( @@ -330,21 +330,6 @@ class OssieRelationship(BaseModel): custom_extensions: Optional[list[OssieCustomExtension]] = None -class OssieSemanticModel(BaseModel): - """Semantic model: the datasets, relationships, and metrics for one business domain.""" - - model_config = ConfigDict(frozen=True, extra="forbid") - - name: str = Field(..., min_length=1) - description: Optional[str] = None - ai_context: Optional[OssieAIContext] = None - datasets: list[OssieDataset] = Field(..., min_length=1) - relationships: Optional[list[OssieRelationship]] = None - # Model-scoped: expressions reference fields by multi-part name (dataset.field) and may span datasets. - metrics: Optional[list[OssieMetric]] = None - custom_extensions: Optional[list[OssieCustomExtension]] = None - - # The specification version every Ossie document declares. Each schema pins it with # `const`, so a document may not declare any other version. Mirrored as a Literal # rather than a plain str, which would let the package serialize a version the @@ -352,8 +337,11 @@ class OssieSemanticModel(BaseModel): OssieVersion = Literal["0.2.0.dev0"] -class OssieDocument(OssieSemanticModel): - """A single semantic model with document metadata at the root.""" +class OssieDocument(BaseModel): + """The root of an Ossie file: the `version` it declares and how it is read and written. + + `OssieSemanticModel` is a semantic model file and `OssieOntology` an ontology file. + """ model_config = ConfigDict(frozen=True, extra="forbid") @@ -390,7 +378,7 @@ def to_ossie_json(self, **kwargs: Any) -> str: return self.model_dump_json(**kwargs) @classmethod - def from_ossie_yaml(cls, text: str) -> OssieDocument: + def from_ossie_yaml(cls, text: str) -> Self: """Parse an Ossie YAML document, checking what pydantic alone cannot. The counterpart of :meth:`to_ossie_yaml`. ``model_validate`` stays lenient, @@ -417,7 +405,7 @@ def from_ossie_yaml(cls, text: str) -> OssieDocument: return cls._from_document(yaml.load(text, Loader=UniqueKeyLoader)) @classmethod - def from_ossie_json(cls, text: str) -> OssieDocument: + def from_ossie_json(cls, text: str) -> Self: """Parse an Ossie JSON document, checking what pydantic alone cannot. The counterpart of :meth:`to_ossie_json`; see :meth:`from_ossie_yaml` for what @@ -426,7 +414,7 @@ def from_ossie_json(cls, text: str) -> OssieDocument: return cls._from_document(json.loads(text, object_pairs_hook=unique_json_object)) @classmethod - def _from_document(cls, data: Any) -> OssieDocument: + def _from_document(cls, data: Any) -> Self: for reason in (_NULL, _NON_FINITE): paths = [path for path, why in _invalid_properties(data) if why == reason] if paths: @@ -435,13 +423,26 @@ def _from_document(cls, data: Any) -> OssieDocument: if len(paths) > _MAX_REPORTED: listed += f", and {len(paths) - _MAX_REPORTED} more" raise ValueError(f"{reason}: {listed}") - if isinstance(data, dict) and "version" in cls.model_fields and "version" not in data: + if isinstance(data, dict) and "version" not in data: # The field carries a default so Python construction needs no version, but # the schema lists it as required, so a document without one is invalid. raise ValueError("document is missing the required property: version") return cls.model_validate(data) +class OssieSemanticModel(OssieDocument): + """Semantic model: the datasets, relationships, and metrics for one business domain.""" + + name: str = Field(..., min_length=1) + description: Optional[str] = None + ai_context: Optional[OssieAIContext] = None + datasets: list[OssieDataset] = Field(..., min_length=1) + relationships: Optional[list[OssieRelationship]] = None + # Model-scoped: expressions reference fields by multi-part name (dataset.field) and may span datasets. + metrics: Optional[list[OssieMetric]] = None + custom_extensions: Optional[list[OssieCustomExtension]] = None + + def _require_any(model: BaseModel, *names: str) -> None: """Mirror a schema `anyOf` of `required` lists: at least one of `names` is set.""" if all(getattr(model, name) is None for name in names): @@ -601,16 +602,13 @@ class OssieOntologyMapping(BaseModel): name: Optional[str] = None description: Optional[str] = None # The schema embeds the whole core document here, `version` included. - semantic_model: OssieDocument + semantic_model: OssieSemanticModel concept_mappings: list[OssieConceptMapping] -class OssieOntologyDocument(BaseModel): +class OssieOntology(OssieDocument): """An ontology: concepts, the relationships among them, and their rules.""" - model_config = ConfigDict(frozen=True, extra="forbid") - - version: OssieVersion = "0.2.0.dev0" name: str description: Optional[str] = None ai_context: Optional[OssieAIContext] = None diff --git a/python/tests/test_ai_context_serialization.py b/python/tests/test_ai_context_serialization.py index 8bc76f77c..eab18dffb 100644 --- a/python/tests/test_ai_context_serialization.py +++ b/python/tests/test_ai_context_serialization.py @@ -20,10 +20,10 @@ import pytest import yaml -from ossie import OssieDocument +from ossie import OssieSemanticModel -def _serialize(document: OssieDocument, format: str, **kwargs) -> dict: +def _serialize(document: OssieSemanticModel, format: str, **kwargs) -> dict: if format == "json": return json.loads(document.to_ossie_json(**kwargs)) return yaml.safe_load(document.to_ossie_yaml(**kwargs)) @@ -65,9 +65,9 @@ def test_null_ai_context_extras_round_trip(document_data: dict, format: str, pat } if format == "json": - document = OssieDocument.from_ossie_json(json.dumps(document_data)) + document = OssieSemanticModel.from_ossie_json(json.dumps(document_data)) else: - document = OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + document = OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) # Unknown properties permit null; omitted declared properties still stay absent. assert _serialize(document, format) == document_data @@ -120,7 +120,7 @@ def test_null_ai_context_extras_respect_selection( "hint": None, "other": None, } - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert _serialize(document, format, **kwargs)["ai_context"] == expected @@ -130,7 +130,7 @@ def test_null_ai_context_extra_does_not_restore_excluded_context( document_data: dict, format: str ) -> None: document_data["ai_context"] = {"hint": None} - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert "ai_context" not in _serialize(document, format, exclude={"ai_context"}) @@ -140,7 +140,7 @@ def test_explicit_none_override_still_includes_declared_properties( document_data: dict, format: str ) -> None: document_data["ai_context"] = {"hint": None} - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert _serialize(document, format, exclude_none=False)["ai_context"] == { "instructions": None, diff --git a/python/tests/test_loading.py b/python/tests/test_loading.py index f03f9231e..f4ad24ef4 100644 --- a/python/tests/test_loading.py +++ b/python/tests/test_loading.py @@ -19,7 +19,7 @@ import pytest import yaml -from ossie import OssieDocument +from ossie import OssieSemanticModel _YAML_MODEL = """\ version: 0.2.0.dev0 @@ -88,7 +88,7 @@ def test_yaml_rejects_duplicate_keys(text: str, key: str | bool | int) -> None: with pytest.raises( yaml.constructor.ConstructorError, match="duplicate key" ) as error: - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) assert f"duplicate key {key!r}" in str(error.value) assert error.value.context_mark is not None @@ -157,7 +157,7 @@ def test_yaml_rejects_duplicate_keys(text: str, key: str | bool | int) -> None: @pytest.mark.parametrize("format_name", ["json", "yaml"]) def test_json_rejects_duplicate_keys(text: str, key: str, format_name: str) -> None: # JSON is also accepted by the YAML entry point. - parse = getattr(OssieDocument, f"from_ossie_{format_name}") + parse = getattr(OssieSemanticModel, f"from_ossie_{format_name}") with pytest.raises( (ValueError, yaml.constructor.ConstructorError), match="duplicate key" ) as error: @@ -168,7 +168,7 @@ def test_json_rejects_duplicate_keys(text: str, key: str, format_name: str) -> N def test_yaml_duplicate_key_diagnostic_identifies_both_locations() -> None: with pytest.raises(yaml.constructor.ConstructorError) as error: - OssieDocument.from_ossie_yaml(_YAML_MODEL + " source: staging.orders\n") + OssieSemanticModel.from_ossie_yaml(_YAML_MODEL + " source: staging.orders\n") assert error.value.context_mark.line == 3 assert error.value.problem_mark.line == 5 @@ -189,7 +189,7 @@ def test_yaml_merge_overrides_and_reused_aliases_are_preserved() -> None: numeric: {"1": string, 1: number} """ ) - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) metadata = document.ai_context.model_dump() assert metadata["first"] == {"note": "original", "other": "kept"} @@ -212,7 +212,7 @@ def test_yaml_merge_overrides_and_reused_aliases_are_preserved() -> None: def test_yaml_merge_precedence_is_preserved(mapping: str, expected: str) -> None: text = _YAML_MODEL + f"ai_context: {mapping}\n" - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) assert document.ai_context.note == expected assert document.ai_context.model_dump(exclude_unset=True) == yaml.safe_load(text)[ @@ -230,7 +230,7 @@ def test_yaml_repeated_merge_aliases_are_preserved() -> None: sequence: {<<: [*second, *first]} literal: {<<: *first, <<: *second, "<<": literal} """ - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) assert document.ai_context.model_dump(exclude_unset=True) == yaml.safe_load(text)[ "ai_context" @@ -247,7 +247,7 @@ def test_yaml_value_keys_are_preserved_in_mappings_and_merges(key: str) -> None: " reused: *original\n" ) - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) assert document.ai_context.original == {"=": "first"} assert document.ai_context.merged == {"=": "second"} assert document.ai_context.reused == {"=": "first"} @@ -264,8 +264,8 @@ def test_same_key_in_separate_objects_is_preserved() -> None: "ai_context": {"first": {"note": 1}, "second": {"note": 2}}, } for parse, text in ( - (OssieDocument.from_ossie_json, json.dumps(data)), - (OssieDocument.from_ossie_yaml, yaml.safe_dump(data)), + (OssieSemanticModel.from_ossie_json, json.dumps(data)), + (OssieSemanticModel.from_ossie_yaml, yaml.safe_dump(data)), ): document = parse(text) assert json.loads(document.to_ossie_json()) == data @@ -273,7 +273,7 @@ def test_same_key_in_separate_objects_is_preserved() -> None: def test_json_entry_point_still_requires_json() -> None: with pytest.raises(json.JSONDecodeError): - OssieDocument.from_ossie_json(_YAML_MODEL) + OssieSemanticModel.from_ossie_json(_YAML_MODEL) @pytest.mark.parametrize("key", ["[first, second]", "{first: second}"]) @@ -281,7 +281,7 @@ def test_yaml_collection_keys_still_raise_a_constructor_error(key: str) -> None: text = _YAML_MODEL + f"ai_context: {{{key}: value}}\n" with pytest.raises(yaml.constructor.ConstructorError, match="unhashable key"): - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) def test_json_number_and_boolean_types_are_preserved() -> None: @@ -291,7 +291,7 @@ def test_json_number_and_boolean_types_are_preserved() -> None: '"ai_context":{"number":1e3,"flag":true,"missing":null}}' ) - document = OssieDocument.from_ossie_json(text) + document = OssieSemanticModel.from_ossie_json(text) assert document.ai_context.number == 1000.0 assert isinstance(document.ai_context.number, float) assert document.ai_context.flag is True diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 040e5f4c9..113da183e 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -31,7 +31,6 @@ OssieDialect, OssieDialectExpression, OssieDimension, - OssieDocument, OssieExpression, OssieField, OssieMetric, @@ -85,7 +84,7 @@ def test_ossie_sql_2026_dialect_survives_serialization(document_data: dict) -> N for item in (field, metric): item["expression"]["dialects"][0]["dialect"] = "OSSIE_SQL_2026" - document = OssieDocument.model_validate(data) + document = OssieSemanticModel.model_validate(data) for item in (document.datasets[0].fields[0], document.metrics[0]): assert item.expression.dialects[0].dialect == "OSSIE_SQL_2026" @@ -94,7 +93,7 @@ def test_ossie_sql_2026_dialect_survives_serialization(document_data: dict) -> N as_yaml = yaml.safe_load(document.to_ossie_yaml()) for serialized in (as_json, as_yaml): assert serialized == data - assert OssieDocument.model_validate(serialized) == document + assert OssieSemanticModel.model_validate(serialized) == document def test_portable_expression_round_trips_and_is_omitted_when_absent( @@ -104,7 +103,7 @@ def test_portable_expression_round_trips_and_is_omitted_when_absent( data = document_data data["datasets"][0]["fields"][0]["expression"]["sql"] = "customer_id" - document = OssieDocument.model_validate(data) + document = OssieSemanticModel.model_validate(data) assert document.datasets[0].fields[0].expression.sql == "customer_id" # A metric whose expression omits the portable form keeps it unset. assert document.metrics[0].expression.sql is None @@ -114,7 +113,7 @@ def test_portable_expression_round_trips_and_is_omitted_when_absent( for serialized in (as_json, as_yaml): assert serialized == data assert "sql" not in serialized["metrics"][0]["expression"] - assert OssieDocument.model_validate(serialized) == document + assert OssieSemanticModel.model_validate(serialized) == document def test_data_type_enum_matches_core_schema() -> None: @@ -133,7 +132,7 @@ def test_data_type_enum_matches_core_schema() -> None: def test_field_and_metric_datatypes_survive_serialization(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) field = document.datasets[0].fields[0] metric = document.metrics[0] @@ -166,12 +165,12 @@ def test_document_serialization_preserves_flat_model_and_metadata( } ], ) - document = OssieDocument.model_validate(data) + document = OssieSemanticModel.model_validate(data) for serialized in (json.loads(document.to_ossie_json()), yaml.safe_load(document.to_ossie_yaml())): assert serialized == data assert "semantic_model" not in serialized - assert OssieDocument.model_validate(serialized) == document + assert OssieSemanticModel.model_validate(serialized) == document @pytest.mark.parametrize( @@ -181,7 +180,7 @@ def test_document_serialization_puts_version_first( serialize: str, document_data: dict ) -> None: data = document_data - document = OssieDocument.model_validate(data) + document = OssieSemanticModel.model_validate(data) serialized = getattr(document, serialize)() if isinstance(serialized, str): @@ -208,7 +207,7 @@ def test_document_serialization_can_omit_version( ) -> None: data = document_data del data["version"] - document = OssieDocument.model_validate(data) + document = OssieSemanticModel.model_validate(data) serialized = getattr(document, serialize)(**options) if isinstance(serialized, str): @@ -219,7 +218,7 @@ def test_document_serialization_can_omit_version( def test_document_serialization_schema_preserves_model_fields() -> None: - schema = OssieDocument.model_json_schema(mode="serialization") + schema = OssieSemanticModel.model_json_schema(mode="serialization") assert schema["properties"]["version"]["type"] == "string" assert schema["properties"]["datasets"]["type"] == "array" @@ -246,7 +245,7 @@ def test_document_rejects_legacy_wrapper( data["semantic_model"] = legacy_value with pytest.raises(ValidationError) as error: - OssieDocument.model_validate(data) + OssieSemanticModel.model_validate(data) assert any( item["loc"] == ("semantic_model",) and item["type"] == "extra_forbidden" @@ -262,7 +261,7 @@ def test_document_requires_root_model_properties( del data[property_name] with pytest.raises(ValidationError): - OssieDocument.model_validate(data) + OssieSemanticModel.model_validate(data) @pytest.mark.parametrize("property_name", ["dialects", "vendors"]) @@ -272,22 +271,13 @@ def test_document_rejects_removed_root_metadata( data = document_data data[property_name] = [] with pytest.raises(ValidationError): - OssieDocument.model_validate(data) - - -def test_embedded_semantic_model_has_no_document_metadata(document_data: dict) -> None: - data = document_data - del data["version"] - - embedded = OssieSemanticModel.model_validate(data) - - assert embedded.model_dump(by_alias=True, exclude_none=True, mode="json") == data + OssieSemanticModel.model_validate(data) def test_document_defaults_version_when_omitted(document_data: dict) -> None: del document_data["version"] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert document.version == "0.2.0.dev0" assert json.loads(document.to_ossie_json())["version"] == "0.2.0.dev0" @@ -303,31 +293,20 @@ def test_version_matches_the_core_schema_const() -> None: schema = json.loads(schema_path.read_text()) const = schema["properties"]["version"]["const"] - assert OssieDocument.model_fields["version"].default == const + assert OssieSemanticModel.model_fields["version"].default == const dataset = {"name": "orders", "source": "db.s.orders"} - assert OssieDocument.model_validate( + assert OssieSemanticModel.model_validate( {"version": const, "name": "m", "datasets": [dataset]} ).version == const with pytest.raises(ValidationError) as error: - OssieDocument.model_validate( + OssieSemanticModel.model_validate( {"version": "9.9.9", "name": "m", "datasets": [dataset]} ) assert any(item["loc"] == ("version",) for item in error.value.errors()) -def test_document_is_a_semantic_model(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) - - assert isinstance(document, OssieSemanticModel) - # `version` is document metadata, absent from the schema's SemanticModel node, - # so the embedded model is built from the same data with it removed. - del document_data["version"] - semantic_model = OssieSemanticModel.model_validate(document_data) - assert document.model_dump(exclude={"version"}) == semantic_model.model_dump() - - @pytest.mark.parametrize( ("container_path", "unknown_key"), [ @@ -365,7 +344,7 @@ def test_every_object_rejects_unknown_fields( container[unknown_key] = "typo" with pytest.raises(ValidationError) as error: - OssieDocument.model_validate(document_data) + OssieSemanticModel.model_validate(document_data) assert any( item["loc"] == (*container_path, unknown_key) @@ -387,7 +366,7 @@ def test_relationship_rejects_unknown_fields(document_data: dict) -> None: ] with pytest.raises(ValidationError) as error: - OssieDocument.model_validate(document_data) + OssieSemanticModel.model_validate(document_data) assert any( item["loc"] == ("relationships", 0, "targeet") @@ -402,7 +381,7 @@ def test_custom_extension_rejects_unknown_fields(document_data: dict) -> None: ] with pytest.raises(ValidationError) as error: - OssieDocument.model_validate(document_data) + OssieSemanticModel.model_validate(document_data) assert any( item["loc"] == ("custom_extensions", 0, "datta") @@ -415,7 +394,7 @@ def test_ai_context_keeps_unknown_fields(document_data: dict) -> None: """`AIContext` is the one open node in the schema, so extras stay allowed.""" document_data["datasets"][0]["ai_context"] = {"synonyms": ["events"], "glossary": "x"} - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert document.datasets[0].ai_context.glossary == "x" assert "glossary" in document.model_dump(by_alias=True, exclude_none=True, mode="json")[ @@ -465,7 +444,7 @@ def test_invalid_datatype_is_rejected(document_data: dict) -> None: field["datatype"] = "timestamp" with pytest.raises(ValidationError): - OssieDocument.model_validate(document_data) + OssieSemanticModel.model_validate(document_data) @pytest.mark.parametrize( @@ -647,7 +626,7 @@ def test_ai_context_object_allows_extra() -> None: def test_ai_context_accepts_string(document_data: dict) -> None: document_data["ai_context"] = "Plain text context" - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) assert document.ai_context == "Plain text context" @@ -655,7 +634,7 @@ def test_ai_context_accepts_object(document_data: dict) -> None: document_data["ai_context"] = { "instructions": "Use this model for analytics" } - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) ai_ctx = document.ai_context assert isinstance(ai_ctx, OssieAIContextObject) assert ai_ctx.instructions == "Use this model for analytics" diff --git a/python/tests/test_ontology_models.py b/python/tests/test_ontology_models.py index 4e1c6cdfd..231352949 100644 --- a/python/tests/test_ontology_models.py +++ b/python/tests/test_ontology_models.py @@ -31,11 +31,12 @@ OssieLinkMapping, OssieMultiplicity, OssieObjectMapping, - OssieOntologyDocument, + OssieOntology, OssieOntologyMapping, OssieOntologyRelationship, OssieReferentMapping, OssieRole, + OssieSemanticModel, OssieVersion, ) @@ -92,28 +93,55 @@ def ontology_data(document_data: dict) -> dict: # --------------------------------------------------------------------------- -def _dump(document: OssieOntologyDocument) -> dict: +def _dump(document: OssieOntology) -> dict: return document.model_dump(mode="json", by_alias=True, exclude_none=True) -def test_example_round_trips() -> None: - data = yaml.safe_load((_REPO / "examples" / "flights.ontology.yaml").read_text()) +@pytest.mark.parametrize("name", ["flights.ontology.yaml", "flights.yaml"]) +def test_example_round_trips(name: str) -> None: + text = (_REPO / "examples" / name).read_text() - assert _dump(OssieOntologyDocument.model_validate(data)) == data + ontology = OssieOntology.from_ossie_yaml(text) + + assert yaml.safe_load(ontology.to_ossie_yaml()) == yaml.safe_load(text) + assert OssieOntology.from_ossie_json(ontology.to_ossie_json()) == ontology + + +def test_documents_share_the_base_document() -> None: + assert issubclass(OssieSemanticModel, OssieDocument) + assert issubclass(OssieOntology, OssieDocument) + + +@pytest.mark.parametrize( + ("text", "error", "message"), + [ + ( + "name: events\nontology:\n- {concept: Event, type: EntityType}\n", + ValueError, + "missing the required property: version", + ), + ("version: 0.2.0.dev0\nname: a\nname: b\n", yaml.constructor.ConstructorError, "duplicate"), + ("version: 0.2.0.dev0\nname: events\ndescription:\n", ValueError, "null.*: description"), + ], + ids=["missing-version", "duplicate-key", "null"], +) +def test_ontology_files_get_the_document_checks(text: str, error: type, message: str) -> None: + with pytest.raises(error, match=message): + OssieOntology.from_ossie_yaml(text) def test_embedded_mapping_carries_a_full_semantic_model_document(ontology_data: dict) -> None: - ontology = OssieOntologyDocument.model_validate(ontology_data) + ontology = OssieOntology.model_validate(ontology_data) semantic_model = ontology.ontology_mappings[0].semantic_model - assert isinstance(semantic_model, OssieDocument) + assert isinstance(semantic_model, OssieSemanticModel) assert semantic_model.version == "0.2.0.dev0" - assert OssieOntologyDocument.model_validate(_dump(ontology)) == ontology + assert OssieOntology.model_validate(_dump(ontology)) == ontology def test_serialization_omits_absent_fields_and_puts_version_first(ontology_data: dict) -> None: del ontology_data["ontology_mappings"] - ontology = OssieOntologyDocument.model_validate(ontology_data) + ontology = OssieOntology.model_validate(ontology_data) assert _dump(ontology) == ontology_data assert list(_dump(ontology))[0] == "version" @@ -144,7 +172,7 @@ def test_models_mirror_the_schema_properties() -> None: the schema but not the model, and vice versa. """ models = { - "": OssieOntologyDocument, + "": OssieOntology, "OntologyComponent": OssieConcept, "Relationship": OssieOntologyRelationship, "Role": OssieRole, @@ -244,7 +272,7 @@ def test_required_properties(ontology_data: dict, path: tuple, key: str) -> None del container[key] with pytest.raises(ValidationError) as error: - OssieOntologyDocument.model_validate(ontology_data) + OssieOntology.model_validate(ontology_data) assert any(item["loc"] == (*path, key) for item in error.value.errors()) @@ -253,11 +281,11 @@ def test_ontology_requires_a_concept(ontology_data: dict) -> None: ontology_data["ontology"] = [] with pytest.raises(ValidationError): - OssieOntologyDocument.model_validate(ontology_data) + OssieOntology.model_validate(ontology_data) def test_invalid_multiplicity_is_rejected(ontology_data: dict) -> None: ontology_data["ontology"][0]["relationships"][0]["multiplicity"] = "ManyToMany" with pytest.raises(ValidationError): - OssieOntologyDocument.model_validate(ontology_data) + OssieOntology.model_validate(ontology_data) diff --git a/python/tests/test_serialization.py b/python/tests/test_serialization.py index 4cf576bb2..d21ce2dd0 100644 --- a/python/tests/test_serialization.py +++ b/python/tests/test_serialization.py @@ -28,7 +28,7 @@ OssieAIContextObject, OssieDataset, OssieDialect, - OssieDocument, + OssieSemanticModel, ) @@ -42,7 +42,7 @@ def test_to_ossie_yaml_uses_alias(document_data: dict) -> None: "to_columns": ["id"], } ] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) output = document.to_ossie_yaml() assert "from: orders" in output assert "from_dataset" not in output @@ -58,7 +58,7 @@ def test_to_ossie_json_uses_alias(document_data: dict) -> None: "to_columns": ["id"], } ] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) output = document.to_ossie_json() parsed = json.loads(output) assert parsed["relationships"][0]["from"] == "orders" @@ -71,7 +71,7 @@ def test_to_ossie_yaml_includes_all_dialects(document_data: dict) -> None: {"dialect": OssieDialect.SNOWFLAKE, "expression": "OCCURRED_AT"}, ] } - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = yaml.safe_load(document.to_ossie_yaml()) assert parsed["datasets"][0]["fields"][0]["expression"]["dialects"] == [ {"dialect": "ANSI_SQL", "expression": "occurred_at"}, @@ -83,7 +83,7 @@ def test_to_ossie_yaml_includes_custom_extension_vendor(document_data: dict) -> document_data["custom_extensions"] = [ {"vendor_name": "DATABRICKS", "data": '{"id":"model-1"}'} ] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = yaml.safe_load(document.to_ossie_yaml()) assert parsed["custom_extensions"] == [ {"vendor_name": "DATABRICKS", "data": '{"id":"model-1"}'} @@ -91,7 +91,7 @@ def test_to_ossie_yaml_includes_custom_extension_vendor(document_data: dict) -> def test_to_ossie_yaml_excludes_none(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) output = document.to_ossie_yaml() parsed = yaml.safe_load(output) assert parsed["name"] == "typed_model" @@ -101,7 +101,7 @@ def test_to_ossie_yaml_excludes_none(document_data: dict) -> None: def test_to_ossie_json_excludes_none(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) output = document.to_ossie_json() parsed = json.loads(output) assert parsed["name"] == "typed_model" @@ -111,7 +111,7 @@ def test_to_ossie_json_excludes_none(document_data: dict) -> None: def test_to_ossie_yaml_validates_as_yaml(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) output = document.to_ossie_yaml() parsed = yaml.safe_load(output) assert parsed["name"] == "typed_model" @@ -121,10 +121,10 @@ def test_to_ossie_json_roundtrip(document_data: dict) -> None: document_data["datasets"][0]["fields"][0]["expression"] = { "dialects": [{"dialect": OssieDialect.DATABRICKS, "expression": "order_id"}] } - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) json_str = document.to_ossie_json() parsed = json.loads(json_str) - document2 = OssieDocument(**parsed) + document2 = OssieSemanticModel(**parsed) assert document2.to_ossie_json() == json_str @@ -138,7 +138,7 @@ def test_to_ossie_yaml_accepts_by_alias_override(document_data: dict) -> None: "to_columns": ["id"], } ] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = yaml.safe_load(document.to_ossie_yaml(by_alias=False)) assert parsed["relationships"][0]["from_dataset"] == "orders" assert "from" not in parsed["relationships"][0] @@ -154,36 +154,36 @@ def test_to_ossie_json_accepts_by_alias_override(document_data: dict) -> None: "to_columns": ["id"], } ] - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = json.loads(document.to_ossie_json(by_alias=False)) assert parsed["relationships"][0]["from_dataset"] == "orders" assert "from" not in parsed["relationships"][0] def test_to_ossie_yaml_accepts_exclude_none_override(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = yaml.safe_load(document.to_ossie_yaml(exclude_none=False)) assert parsed["description"] is None assert parsed["relationships"] is None def test_to_ossie_json_accepts_exclude_none_override(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) parsed = json.loads(document.to_ossie_json(exclude_none=False)) assert parsed["description"] is None assert parsed["relationships"] is None def test_from_ossie_yaml_round_trips(document_data: dict) -> None: - text = OssieDocument.model_validate(document_data).to_ossie_yaml() + text = OssieSemanticModel.model_validate(document_data).to_ossie_yaml() - assert OssieDocument.from_ossie_yaml(text).to_ossie_yaml() == text + assert OssieSemanticModel.from_ossie_yaml(text).to_ossie_yaml() == text def test_from_ossie_json_round_trips(document_data: dict) -> None: - text = OssieDocument.model_validate(document_data).to_ossie_json() + text = OssieSemanticModel.model_validate(document_data).to_ossie_json() - assert OssieDocument.from_ossie_json(text).to_ossie_json() == text + assert OssieSemanticModel.from_ossie_json(text).to_ossie_json() == text @pytest.mark.parametrize( @@ -213,18 +213,18 @@ def test_loaders_reject_a_property_set_to_null(document_data: dict, mutate, repo text = yaml.safe_dump(document_data) with pytest.raises(ValueError, match="set to null") as error: - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) assert _reported_paths(error.value) == {reported} with pytest.raises(ValueError, match="set to null"): - OssieDocument.from_ossie_json(json.dumps(document_data)) + OssieSemanticModel.from_ossie_json(json.dumps(document_data)) def test_loaders_allow_a_null_inside_free_form_ai_context(document_data: dict) -> None: """`ai_context` sets `additionalProperties: true`, so an extra key may hold null.""" document_data["datasets"][0]["ai_context"] = {"instructions": "use me", "vendor_hint": None} - document = OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + document = OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) ai_context = document.datasets[0].ai_context assert isinstance(ai_context, OssieAIContextObject) @@ -240,7 +240,7 @@ def test_model_validate_stays_lenient_about_null(document_data: dict) -> None: """ document_data["description"] = None - assert OssieDocument.model_validate(document_data).description is None + assert OssieSemanticModel.model_validate(document_data).description is None def _reported_paths(error: Exception) -> set[str]: @@ -394,7 +394,7 @@ def test_the_sample_document_sets_every_declared_field() -> None: is a different property from `description` on a dataset, and a sample that set only one of them would otherwise look complete. """ - missing = _missing_properties(OssieDocument, _fully_populated_document(), "") + missing = _missing_properties(OssieSemanticModel, _fully_populated_document(), "") assert missing == [], "add these to _fully_populated_document()" @@ -408,7 +408,7 @@ def test_every_property_in_a_document_rejects_null() -> None: document = _fully_populated_document() _set_null(document, path) with pytest.raises(ValueError, match="set to null"): - OssieDocument.from_ossie_yaml(yaml.safe_dump(document)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document)) def test_every_null_is_reported_not_just_the_first(document_data: dict) -> None: @@ -416,7 +416,7 @@ def test_every_null_is_reported_not_just_the_first(document_data: dict) -> None: document_data["datasets"][0]["description"] = None with pytest.raises(ValueError) as error: - OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) assert _reported_paths(error.value) == {"description", "datasets[0].description"} @@ -425,13 +425,13 @@ def test_ai_context_may_be_a_plain_string(document_data: dict) -> None: """`AIContext` is a union, so the walk must tolerate the string branch.""" document_data["ai_context"] = "free text" - assert OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)).ai_context == "free text" + assert OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)).ai_context == "free text" @pytest.mark.parametrize("text", ["[]\n", "null\n", "42\n", ""]) def test_loaders_reject_a_document_that_is_not_a_mapping(text: str) -> None: with pytest.raises(ValidationError): - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) def test_a_null_inside_an_array_is_rejected_by_the_field_type(document_data: dict) -> None: @@ -443,7 +443,7 @@ def test_a_null_inside_an_array_is_rejected_by_the_field_type(document_data: dic document_data["datasets"][0]["primary_key"] = [None] with pytest.raises(ValidationError): - OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) def test_loaders_reject_a_cyclic_alias() -> None: @@ -462,7 +462,7 @@ def test_loaders_reject_a_cyclic_alias() -> None: ) with pytest.raises(ValueError, match="cyclic alias"): - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) def test_loaders_accept_an_alias_reused_without_a_cycle(document_data: dict) -> None: @@ -481,7 +481,7 @@ def test_loaders_accept_an_alias_reused_without_a_cycle(document_data: dict) -> " ai_context: *ctx\n" ) - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) second = document.datasets[1].ai_context assert isinstance(second, OssieAIContextObject) @@ -505,7 +505,7 @@ def test_loaders_reject_a_cycle_inside_free_form_ai_context() -> None: ) with pytest.raises(ValueError, match="cyclic alias"): - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) def test_a_reused_alias_graph_does_not_expand_exponentially() -> None: @@ -553,7 +553,7 @@ def test_a_null_under_a_reused_anchor_is_reported_once() -> None: ) with pytest.raises(ValueError) as error: - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) assert _reported_paths(error.value) == {"datasets[0].fields[0].label"} @@ -569,7 +569,7 @@ def test_a_long_list_of_nulls_is_truncated_with_a_count() -> None: } with pytest.raises(ValueError) as error: - OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) assert "and 10 more" in str(error.value) assert str(error.value).count("description") == models._MAX_REPORTED @@ -593,7 +593,7 @@ def test_loaders_reject_a_non_finite_number_from_yaml(text: str) -> None: document = "version: 0.2.0.dev0\nname: m\ndatasets:\n- name: o\n source: db.s.o\n" + text with pytest.raises(ValueError, match="cannot round-trip"): - OssieDocument.from_ossie_yaml(document) + OssieSemanticModel.from_ossie_yaml(document) @pytest.mark.parametrize("constant", ["NaN", "Infinity", "-Infinity"]) @@ -605,7 +605,7 @@ def test_loaders_reject_a_non_standard_json_constant(constant: str) -> None: ) with pytest.raises(ValueError, match="cannot round-trip"): - OssieDocument.from_ossie_json(text) + OssieSemanticModel.from_ossie_json(text) def test_loaders_require_the_version_property() -> None: @@ -617,12 +617,12 @@ def test_loaders_require_the_version_property() -> None: text = "name: m\ndatasets:\n- name: o\n source: db.s.o\n" with pytest.raises(ValueError, match="missing the required property: version"): - OssieDocument.from_ossie_yaml(text) + OssieSemanticModel.from_ossie_yaml(text) def test_direct_construction_still_defaults_the_version() -> None: """Only a document must declare it; building a model in Python need not.""" - document = OssieDocument( + document = OssieSemanticModel( name="m", datasets=[OssieDataset(name="o", source="db.s.o")] ) @@ -649,7 +649,7 @@ def test_free_form_ai_context_content_is_passed_through() -> None: " numbered: {1: one}\n" ) - document = OssieDocument.from_ossie_yaml(text) + document = OssieSemanticModel.from_ossie_yaml(text) extra = document.datasets[0].ai_context assert isinstance(extra, OssieAIContextObject) @@ -669,7 +669,7 @@ def test_the_null_diagnostic_reads_as_a_sentence(document_data: dict) -> None: document_data["description"] = None with pytest.raises(ValueError) as error: - OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) assert str(error.value) == ( "property set to null, which the schema does not allow; omit it instead: description" @@ -681,7 +681,7 @@ def test_the_non_finite_diagnostic_reads_as_a_sentence(document_data: dict) -> N document_data["datasets"][0]["ai_context"] = {"score": float("nan")} with pytest.raises(ValueError) as error: - OssieDocument.from_ossie_yaml(yaml.safe_dump(document_data)) + OssieSemanticModel.from_ossie_yaml(yaml.safe_dump(document_data)) assert str(error.value) == ( "property holds a number JSON cannot represent, so it cannot round-trip: " diff --git a/python/tests/test_validation.py b/python/tests/test_validation.py index f23353254..a183616d7 100644 --- a/python/tests/test_validation.py +++ b/python/tests/test_validation.py @@ -27,7 +27,6 @@ OssieDialect, OssieDialectExpression, OssieDimension, - OssieDocument, OssieExpression, OssieField, OssieMetric, @@ -233,7 +232,7 @@ def test_frozen_field(make_expression: Callable[[str], OssieExpression]) -> None def test_frozen_document(document_data: dict) -> None: - document = OssieDocument.model_validate(document_data) + document = OssieSemanticModel.model_validate(document_data) with pytest.raises(ValidationError): document.version = "new.version" From bc51ed99a50d3de792d9f45a2174ad48bfb14abf Mon Sep 17 00:00:00 2001 From: Andrew Nepogoda Date: Wed, 28 Oct 2026 13:21:06 +0100 Subject: [PATCH 3/5] docs(python): rewrap the OssieDocument paragraph in the README --- python/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/README.md b/python/README.md index 3e45ef051..6cec47c0b 100644 --- a/python/README.md +++ b/python/README.md @@ -45,8 +45,9 @@ overrides of merged keys. the relationships among them (`OssieOntologyRelationship`, distinct from the dataset join `OssieRelationship`), and, deprecated, embedded `ontology_mappings`. -`OssieSemanticModel` and `OssieOntology` both extend `OssieDocument`, which holds `version` and the `to_ossie_*` and `from_ossie_*` methods. An ontology -file therefore loads with `OssieOntology.from_ossie_yaml(text)`, under the same checks. +`OssieSemanticModel` and `OssieOntology` both extend `OssieDocument`, which holds +`version` and the `to_ossie_*` and `from_ossie_*` methods. An ontology file +therefore loads with `OssieOntology.from_ossie_yaml(text)`, under the same checks. ## Development From 57de734a0d965668e9988f6dfcc029724973382f Mon Sep 17 00:00:00 2001 From: Andrew Nepogoda Date: Wed, 28 Oct 2026 13:56:22 +0100 Subject: [PATCH 4/5] refactor(python): rename OssieOntology to OssieOntologyModel Pair the ontology file class with OssieSemanticModel, and avoid a clash with the ontology converter's runtime OssieOntology class in ossie_ontology/model.py, so both can be imported without an alias. --- python/README.md | 6 +++--- python/src/ossie/__init__.py | 4 ++-- python/src/ossie/models.py | 4 ++-- python/tests/test_ontology_models.py | 26 +++++++++++++------------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/python/README.md b/python/README.md index 6cec47c0b..ac09bb3ad 100644 --- a/python/README.md +++ b/python/README.md @@ -41,13 +41,13 @@ including keys in nested objects, so an earlier value is not silently lost. The YAML loader preserves aliases, repeated merge directives and explicit overrides of merged keys. -`OssieOntology` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), +`OssieOntologyModel` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), the relationships among them (`OssieOntologyRelationship`, distinct from the dataset join `OssieRelationship`), and, deprecated, embedded `ontology_mappings`. -`OssieSemanticModel` and `OssieOntology` both extend `OssieDocument`, which holds +`OssieSemanticModel` and `OssieOntologyModel` both extend `OssieDocument`, which holds `version` and the `to_ossie_*` and `from_ossie_*` methods. An ontology file -therefore loads with `OssieOntology.from_ossie_yaml(text)`, under the same checks. +therefore loads with `OssieOntologyModel.from_ossie_yaml(text)`, under the same checks. ## Development diff --git a/python/src/ossie/__init__.py b/python/src/ossie/__init__.py index 534d7c3c6..4c6e10359 100644 --- a/python/src/ossie/__init__.py +++ b/python/src/ossie/__init__.py @@ -41,8 +41,8 @@ OssieMetric, OssieMultiplicity, OssieObjectMapping, - OssieOntology, OssieOntologyMapping, + OssieOntologyModel, OssieOntologyRelationship, OssieReferentMapping, OssieRelationship, @@ -71,8 +71,8 @@ "OssieMetric", "OssieMultiplicity", "OssieObjectMapping", - "OssieOntology", "OssieOntologyMapping", + "OssieOntologyModel", "OssieOntologyRelationship", "OssieReferentMapping", "OssieRelationship", diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index ed4277f42..668bbea22 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -340,7 +340,7 @@ class OssieRelationship(BaseModel): class OssieDocument(BaseModel): """The root of an Ossie file: the `version` it declares and how it is read and written. - `OssieSemanticModel` is a semantic model file and `OssieOntology` an ontology file. + `OssieSemanticModel` is a semantic model file and `OssieOntologyModel` an ontology file. """ model_config = ConfigDict(frozen=True, extra="forbid") @@ -606,7 +606,7 @@ class OssieOntologyMapping(BaseModel): concept_mappings: list[OssieConceptMapping] -class OssieOntology(OssieDocument): +class OssieOntologyModel(OssieDocument): """An ontology: concepts, the relationships among them, and their rules.""" name: str diff --git a/python/tests/test_ontology_models.py b/python/tests/test_ontology_models.py index 231352949..3f9fb30aa 100644 --- a/python/tests/test_ontology_models.py +++ b/python/tests/test_ontology_models.py @@ -31,8 +31,8 @@ OssieLinkMapping, OssieMultiplicity, OssieObjectMapping, - OssieOntology, OssieOntologyMapping, + OssieOntologyModel, OssieOntologyRelationship, OssieReferentMapping, OssieRole, @@ -93,7 +93,7 @@ def ontology_data(document_data: dict) -> dict: # --------------------------------------------------------------------------- -def _dump(document: OssieOntology) -> dict: +def _dump(document: OssieOntologyModel) -> dict: return document.model_dump(mode="json", by_alias=True, exclude_none=True) @@ -101,15 +101,15 @@ def _dump(document: OssieOntology) -> dict: def test_example_round_trips(name: str) -> None: text = (_REPO / "examples" / name).read_text() - ontology = OssieOntology.from_ossie_yaml(text) + ontology = OssieOntologyModel.from_ossie_yaml(text) assert yaml.safe_load(ontology.to_ossie_yaml()) == yaml.safe_load(text) - assert OssieOntology.from_ossie_json(ontology.to_ossie_json()) == ontology + assert OssieOntologyModel.from_ossie_json(ontology.to_ossie_json()) == ontology def test_documents_share_the_base_document() -> None: assert issubclass(OssieSemanticModel, OssieDocument) - assert issubclass(OssieOntology, OssieDocument) + assert issubclass(OssieOntologyModel, OssieDocument) @pytest.mark.parametrize( @@ -127,21 +127,21 @@ def test_documents_share_the_base_document() -> None: ) def test_ontology_files_get_the_document_checks(text: str, error: type, message: str) -> None: with pytest.raises(error, match=message): - OssieOntology.from_ossie_yaml(text) + OssieOntologyModel.from_ossie_yaml(text) def test_embedded_mapping_carries_a_full_semantic_model_document(ontology_data: dict) -> None: - ontology = OssieOntology.model_validate(ontology_data) + ontology = OssieOntologyModel.model_validate(ontology_data) semantic_model = ontology.ontology_mappings[0].semantic_model assert isinstance(semantic_model, OssieSemanticModel) assert semantic_model.version == "0.2.0.dev0" - assert OssieOntology.model_validate(_dump(ontology)) == ontology + assert OssieOntologyModel.model_validate(_dump(ontology)) == ontology def test_serialization_omits_absent_fields_and_puts_version_first(ontology_data: dict) -> None: del ontology_data["ontology_mappings"] - ontology = OssieOntology.model_validate(ontology_data) + ontology = OssieOntologyModel.model_validate(ontology_data) assert _dump(ontology) == ontology_data assert list(_dump(ontology))[0] == "version" @@ -172,7 +172,7 @@ def test_models_mirror_the_schema_properties() -> None: the schema but not the model, and vice versa. """ models = { - "": OssieOntology, + "": OssieOntologyModel, "OntologyComponent": OssieConcept, "Relationship": OssieOntologyRelationship, "Role": OssieRole, @@ -272,7 +272,7 @@ def test_required_properties(ontology_data: dict, path: tuple, key: str) -> None del container[key] with pytest.raises(ValidationError) as error: - OssieOntology.model_validate(ontology_data) + OssieOntologyModel.model_validate(ontology_data) assert any(item["loc"] == (*path, key) for item in error.value.errors()) @@ -281,11 +281,11 @@ def test_ontology_requires_a_concept(ontology_data: dict) -> None: ontology_data["ontology"] = [] with pytest.raises(ValidationError): - OssieOntology.model_validate(ontology_data) + OssieOntologyModel.model_validate(ontology_data) def test_invalid_multiplicity_is_rejected(ontology_data: dict) -> None: ontology_data["ontology"][0]["relationships"][0]["multiplicity"] = "ManyToMany" with pytest.raises(ValidationError): - OssieOntology.model_validate(ontology_data) + OssieOntologyModel.model_validate(ontology_data) From ef3013fea066fc67a0da890a06524a092d1d3cc4 Mon Sep 17 00:00:00 2001 From: Andrew Nepogoda Date: Wed, 28 Oct 2026 17:45:48 +0100 Subject: [PATCH 5/5] refactor(python): rename OssieConcept to OssieConceptComponent Match the ConceptComponent name the ontology converter already uses in ossie_ontology.spec, so the migration keeps the familiar name. --- python/README.md | 7 ++++--- python/src/ossie/__init__.py | 4 ++-- python/src/ossie/models.py | 4 ++-- python/tests/test_ontology_models.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/python/README.md b/python/README.md index ac09bb3ad..ef1b5091c 100644 --- a/python/README.md +++ b/python/README.md @@ -41,9 +41,10 @@ including keys in nested objects, so an earlier value is not silently lost. The YAML loader preserves aliases, repeated merge directives and explicit overrides of merged keys. -`OssieOntologyModel` mirrors `ontology/ontology.json`: concepts (`OssieConcept`), -the relationships among them (`OssieOntologyRelationship`, distinct from the dataset -join `OssieRelationship`), and, deprecated, embedded `ontology_mappings`. +`OssieOntologyModel` mirrors `ontology/ontology.json`: concepts +(`OssieConceptComponent`), the relationships among them +(`OssieOntologyRelationship`, distinct from the dataset join `OssieRelationship`), +and, deprecated, embedded `ontology_mappings`. `OssieSemanticModel` and `OssieOntologyModel` both extend `OssieDocument`, which holds `version` and the `to_ossie_*` and `from_ossie_*` methods. An ontology file diff --git a/python/src/ossie/__init__.py b/python/src/ossie/__init__.py index 4c6e10359..4454c63c9 100644 --- a/python/src/ossie/__init__.py +++ b/python/src/ossie/__init__.py @@ -25,7 +25,7 @@ from ossie.models import ( OssieAIContext, OssieAIContextObject, - OssieConcept, + OssieConceptComponent, OssieConceptMapping, OssieConceptType, OssieCustomExtension, @@ -55,7 +55,7 @@ __all__ = [ "OssieAIContext", "OssieAIContextObject", - "OssieConcept", + "OssieConceptComponent", "OssieConceptMapping", "OssieConceptType", "OssieCustomExtension", diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index 668bbea22..10db6c58c 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -502,7 +502,7 @@ class OssieOntologyRelationship(BaseModel): iri: Optional[str] = None -class OssieConcept(BaseModel): +class OssieConceptComponent(BaseModel): """A concept and the relationships where it plays the first role. The schema calls this node `OntologyComponent`. @@ -613,7 +613,7 @@ class OssieOntologyModel(OssieDocument): description: Optional[str] = None ai_context: Optional[OssieAIContext] = None requires: Optional[list[str]] = None - ontology: list[OssieConcept] = Field(..., min_length=1) + ontology: list[OssieConceptComponent] = Field(..., min_length=1) # Deprecated by the schema; see `OssieOntologyMapping`. ontology_mappings: Optional[list[OssieOntologyMapping]] = None # Namespace prefix -> the IRI it abbreviates, for `iri` values written as QNames. diff --git a/python/tests/test_ontology_models.py b/python/tests/test_ontology_models.py index 3f9fb30aa..44462b933 100644 --- a/python/tests/test_ontology_models.py +++ b/python/tests/test_ontology_models.py @@ -24,7 +24,7 @@ from pydantic import ValidationError from ossie import ( - OssieConcept, + OssieConceptComponent, OssieConceptMapping, OssieConceptType, OssieDocument, @@ -173,7 +173,7 @@ def test_models_mirror_the_schema_properties() -> None: """ models = { "": OssieOntologyModel, - "OntologyComponent": OssieConcept, + "OntologyComponent": OssieConceptComponent, "Relationship": OssieOntologyRelationship, "Role": OssieRole, "OntologyMap": OssieOntologyMapping,