Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/converter-ontology-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ on:
paths:
- 'converters/ontology/**'
- 'core-spec/**'
- 'ontology/**'
- 'examples/**'
- '.github/workflows/converter-ontology-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'converters/ontology/**'
- 'core-spec/**'
- 'ontology/**'
- 'examples/**'
- '.github/workflows/converter-ontology-ci.yml'

Expand Down Expand Up @@ -64,4 +66,4 @@ jobs:
- name: Unit Tests
working-directory: converters/ontology
run: |
uv run pytest
uv run pytest
2 changes: 2 additions & 0 deletions .github/workflows/converter-orionbelt-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ on:
paths:
- 'converters/orionbelt/**'
- 'core-spec/**'
- 'ontology/**'
- 'examples/**'
- '.github/workflows/converter-orionbelt-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'converters/orionbelt/**'
- 'core-spec/**'
- 'ontology/**'
- 'examples/**'
- '.github/workflows/converter-orionbelt-ci.yml'

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/validation-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ on:
- "validation/**"
- "python/**"
- "core-spec/**"
- "ontology/**"
- "examples/flights.yaml"
- "examples/tpcds_semantic_model.yaml"
- ".github/workflows/validation-ci.yml"
pull_request:
Expand All @@ -34,6 +36,8 @@ on:
- "validation/**"
- "python/**"
- "core-spec/**"
- "ontology/**"
- "examples/flights.yaml"
- "examples/tpcds_semantic_model.yaml"
- ".github/workflows/validation-ci.yml"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def _convert_relationship(rel: Relationship) -> SpecRelationship:

def _convert_semantic_model(semantic_model: SemanticModel) -> SpecSemanticModel:
return SpecSemanticModel(
version=semantic_model.version,
name=semantic_model.name,
description=semantic_model.description,
ai_context=semantic_model.ai_context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def convert(
schema_name: str = "palantir",
) -> OssieOntology:
ontology = OntologyComponent()
model = OssieOntology(name="Palantir model", ontology=ontology, version="0.1.0")
model = OssieOntology(name="Palantir model", ontology=ontology, version="0.2.0.dev0")

semantic_model = SemanticModel(name="Palantir semantic model")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ def _convert_relationship(

def _convert_semantic_model(self, lm_spec: SpecSemanticModel) -> SemanticModel:
semantic_model = SemanticModel(
version=lm_spec.version,
name=lm_spec.name,
description=lm_spec.description,
ai_context=lm_spec.ai_context,
Expand Down
14 changes: 12 additions & 2 deletions converters/ontology/src/ossie_ontology/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,8 @@ def custom_extensions(self) -> list[CustomExtension]:


class SemanticModel:
"""Bundle of datasets, join paths and metrics. One or more SemanticModels
can feed a single OntologyMapping (see spec)."""
"""Versioned core semantic model embedded in a single OntologyMapping."""
_version: str
_name: str
_description: str | None
_ai_context: AiContext | None
Expand All @@ -704,7 +704,13 @@ def __init__(
description: str | None = None,
ai_context: AiContext | None = None,
custom_extensions: list[CustomExtension] | None = None,
version: str = "0.2.0.dev0",
Comment thread
flyrain marked this conversation as resolved.
):
if version != "0.2.0.dev0":
raise ValueError(
f"Unsupported semantic model version {version!r}; expected '0.2.0.dev0'"
)
self._version = version
self._name = name
self._description = description
self._ai_context = ai_context
Expand All @@ -716,6 +722,10 @@ def __init__(
self._join_path_name_map = {}
self._metric_name_map = {}

@property
def version(self) -> str:
return self._version

@property
def name(self) -> str:
return self._name
Expand Down
9 changes: 3 additions & 6 deletions converters/ontology/src/ossie_ontology/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,12 @@ class Metric(OssieObject):


class SemanticModel(OssieObject):
"""Model contents: datasets plus the join paths and metrics defined over them.

OntologyMapping embeds these contents under ``semantic_model``; standalone
core documents place them at the root alongside document metadata.
"""
"""Complete core document embedded under OntologyMapping.semantic_model."""
version: Literal["0.2.0.dev0"]
Comment thread
flyrain marked this conversation as resolved.
name: str
description: str | None = None
ai_context: AiContext | None = None
datasets: list[Dataset] = Field(default_factory=list)
datasets: list[Dataset] = Field(min_length=1)
relationships: list[JoinPath] = Field(default_factory=list)
metrics: list[Metric] = Field(default_factory=list)
custom_extensions: list[CustomExtension] = Field(default_factory=list)
Expand Down
1 change: 1 addition & 0 deletions converters/ontology/tests/fixtures/flights.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ ontology:
ontology_mappings:
- name: flights_mapping
semantic_model:
version: 0.2.0.dev0
name: Flights semantic model
datasets:
- name: RUNWAY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ ontology:
ontology_mappings:
- name: flights_mapping
semantic_model:
version: 0.2.0.dev0
name: Flights semantic model
datasets:
- name: RUNWAY
Expand Down
57 changes: 57 additions & 0 deletions converters/ontology/tests/test_ossie_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import pytest
import yaml
from pydantic import ValidationError

from ossie_ontology.converter.ossie_to_spec.converter import (
OssieToSpecConverter,
Expand All @@ -49,6 +50,62 @@ def test_parse_returns_model_with_metadata(flights_model):
assert flights_model.description == "Ontology of flights into and out of airports."


def test_embedded_core_document_version_survives_roundtrip(flights_model, tmp_path):
assert all(
mapping.semantic_model.version == "0.2.0.dev0"
for mapping in flights_model.ontology_mappings
)

exported = OssieToSpecConverter.convert(flights_model).dump_yaml()
document = yaml.safe_load(exported)
assert all(
mapping["semantic_model"]["version"] == "0.2.0.dev0"
for mapping in document["ontology_mappings"]
)

path = tmp_path / "roundtrip.yaml"
path.write_text(exported)
reparsed = OssieParser().parse(path)
assert all(
mapping.semantic_model.version == "0.2.0.dev0"
for mapping in reparsed.ontology_mappings
)


@pytest.mark.parametrize("version", ["missing", "0.1.0", "0.2.0", None, 2])
def test_parse_rejects_invalid_embedded_version(flights_path, tmp_path, version):
document = yaml.safe_load(flights_path.read_text())
model = document["ontology_mappings"][0]["semantic_model"]
if version == "missing":
del model["version"]
else:
model["version"] = version
path = tmp_path / "invalid.yaml"
path.write_text(yaml.safe_dump(document))

with pytest.raises(ValidationError) as exc:
OssieParser().parse(path)

assert exc.value.errors()[0]["loc"] == ("ontology_mappings", 0, "semantic_model", "version")


@pytest.mark.parametrize("missing", [True, False], ids=["missing", "empty"])
def test_parse_rejects_missing_or_empty_datasets(flights_path, tmp_path, missing):
document = yaml.safe_load(flights_path.read_text())
model = document["ontology_mappings"][0]["semantic_model"]
if missing:
del model["datasets"]
else:
model["datasets"] = []
path = tmp_path / "invalid.yaml"
path.write_text(yaml.safe_dump(document))

with pytest.raises(ValidationError) as exc:
OssieParser().parse(path)

assert exc.value.errors()[0]["loc"] == ("ontology_mappings", 0, "semantic_model", "datasets")


def test_parse_returns_populated_ontology(flights_model):
ontology = flights_model.ontology
# Built-in concepts (String, Integer, Decimal, ...) are always present on top
Expand Down
17 changes: 17 additions & 0 deletions converters/ontology/tests/test_palantir_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,23 @@ def _identifier_columns(model: OssieOntology, concept_name: str) -> list[str]:
return columns


# ----- document metadata ------------------------------------------------

def test_generated_semantic_model_has_core_document_version(tmp_path: Path):
model = _convert(
tmp_path,
[_object_type("widget", "Widget", status="active")],
datasets=[_dataset("widget", ["widget_id"])],
)

document = OssieToSpecConverter.convert(model).dump_dict()

assert document["version"] == "0.2.0.dev0"
[mapping] = document["ontology_mappings"]
assert mapping["semantic_model"]["version"] == "0.2.0.dev0"
assert mapping["semantic_model"]["datasets"]


# ----- object type statuses ---------------------------------------------

def test_default_policy_admits_active_endorsed_and_intermediary(tmp_path: Path):
Expand Down
62 changes: 62 additions & 0 deletions converters/ontology/tests/test_semantic_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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.

"""Core document constraints at runtime construction and export."""

import pytest
from pydantic import ValidationError

from ossie_ontology.converter.ossie_to_spec.converter import OssieToSpecConverter
from ossie_ontology.model import (
Dataset,
OntologyComponent,
OntologyMapping,
OssieOntology,
SemanticModel,
)


@pytest.mark.parametrize("version", ["0.1.0", "0.2.0", "", None, 2])
def test_rejects_unsupported_version_at_construction(version):
with pytest.raises(ValueError, match="Unsupported semantic model version"):
SemanticModel(name="sales", version=version)


@pytest.mark.parametrize("version_args", [{}, {"version": "0.2.0.dev0"}])
def test_requires_datasets_at_export_but_allows_incremental_construction(version_args):
model = SemanticModel(name="sales", **version_args)
ontology = OssieOntology(
name="sales", version="0.2.0.dev0", ontology=OntologyComponent()
)
ontology.add_ontology_mapping(
OntologyMapping(
name="sales_mapping", ontology=ontology.ontology, semantic_model=model
)
)

with pytest.raises(ValidationError) as exc:
OssieToSpecConverter.convert(ontology)
assert exc.value.errors()[0]["loc"] == ("datasets",)

model.add_dataset(Dataset(name="orders", source="sales.public.orders", fields=[]))
exported = OssieToSpecConverter.convert(ontology).dump_dict()

assert exported["ontology_mappings"][0]["semantic_model"] == {
"version": "0.2.0.dev0",
"name": "sales",
"datasets": [{"name": "orders", "source": "sales.public.orders"}],
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ ontology_mappings:
| `join.joinType` | `Relationship.multiplicity` | `many-to-one`→`ManyToOne`, `one-to-one`→`OneToOne` |
| `column.primaryKey` | entity `object_mappings[].expression` | `<table>.<pk_code>`; identifies the entity |
| `join.columnsFrom` (FK) | `link_mappings[].object_mapping.expression` | `<table_A>.<fk_code>`; binds the relationship to its far role |
| whole core model | `ontology_mappings[].semantic_model` | embedded from `OBMLtoOssie.convert()` without standalone document metadata (`version`, `dialects`, `vendors`) |
| whole core model | `ontology_mappings[].semantic_model` | complete output from `OBMLtoOssie.convert()`|

`<table>` is the final identifier of the dataset `source` (e.g. `db.schema.t` → `t`),
falling back to the dataset name when `source` has no dotted physical table.
Expand Down
1 change: 0 additions & 1 deletion converters/orionbelt/src/ossie_orionbelt/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ def convert(self) -> dict:
ai_instructions=self.ai_instructions,
)
sem_model = core_conv.convert()
sem_model.pop("version", None)
self.warnings.extend(core_conv.warnings)

data_objects = self.obml.get("dataObjects", {}) or {}
Expand Down
6 changes: 5 additions & 1 deletion converters/orionbelt/tests/test_ossie_converter_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ def test_embeds_core_semantic_model(self) -> None:
omap = doc["ontology_mappings"][0]
assert omap["name"] == "sales_map"
assert "semantic_model" in omap
assert not {"version", "dialects", "vendors"} & set(omap["semantic_model"])
assert omap["semantic_model"]["version"] == "0.2.0.dev0"
assert not {"dialects", "vendors"} & set(omap["semantic_model"])
assert omap["semantic_model"]["name"] == "sales"
assert {d["name"] for d in omap["semantic_model"]["datasets"]} == {
"Customers",
"Products",
"Orders",
}
result = conv.validate_ossie(omap["semantic_model"])
assert result.valid, result.schema_errors + result.semantic_errors
assert not result.semantic_warnings

def test_concept_mappings_bind_keys_and_fks(self) -> None:
doc = conv.OBMLtoOssieOntology(_OBML, model_name="sales").convert()
Expand Down
6 changes: 2 additions & 4 deletions core-spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,8 @@ model array cannot produce a valid model document. Preserve model contents and
custom extensions; never silently select only the first model or overwrite a file
when splitting a document.

The reusable `$defs/SemanticModel` schema still describes model contents without
standalone document metadata. In particular, an ontology map continues to embed
those contents under its `semantic_model` property. This standalone document
change does not rename or flatten that ontology property.
For [ontology maps](../ontology/ontology.md#ontology-mappings), each embedded
model must be a complete core document.

---

Expand Down
1 change: 1 addition & 0 deletions examples/flights.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ ontology:
ontology_mappings:
- name: flights_mapping
semantic_model:
version: 0.2.0.dev0
name: Flights semantic model
datasets:
- name: RUNWAY
Expand Down
2 changes: 1 addition & 1 deletion ontology/ontology.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@
"description": "Human-readable description of this ontology map"
},
"semantic_model": {
"$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/ossie-schema.json#/$defs/SemanticModel"
"$ref": "https://raw.githubusercontent.com/apache/ossie/main/core-spec/ossie-schema.json"
Comment thread
flyrain marked this conversation as resolved.
Comment thread
flyrain marked this conversation as resolved.
},
"concept_mappings": {
"type": "array",
Expand Down
Loading
Loading