Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/validation-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,6 @@ jobs:
uv run validation/validate.py examples/flights.ontology.yaml --schema ontology/ontology.json
uv run validation/validate.py examples/flights.semantic_model.yaml
uv run validation/validate.py examples/flights.mapping.yaml --schema ontology/mapping.json

- name: Validate FOAF example
run: uv run validation/validate.py examples/foaf_owl_import.yaml --schema ontology/ontology.json
8 changes: 8 additions & 0 deletions converters/ontology/src/ossie_ontology/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class OssieObject(BaseModel):
# like `instructions`, `synonyms`, `examples` (per core.md).
AiContext = str | dict[str, Any]

# Open set of properties the spec does not model (e.g. OWL/RDF annotations kept
# for round-tripping). Keys are free-form and values may be any JSON.
Annotations = dict[str, Any]


class CustomExtension(OssieObject):
"""Vendor-specific metadata attached to any logical-model element (core.md).
Expand All @@ -59,6 +63,7 @@ class Role(OssieObject):
concept plays multiple roles in the same relationship."""
concept: str
name: str | None = None
annotations: Annotations = Field(default_factory=dict)


class Relationship(OssieObject):
Expand All @@ -78,6 +83,7 @@ class Relationship(OssieObject):
multiplicity: Literal["OneToOne", "ManyToOne"] | None = None
derived_by: list[str] = Field(default_factory=list)
requires: list[str] = Field(default_factory=list)
annotations: Annotations = Field(default_factory=dict)


class ConceptComponent(OssieObject):
Expand Down Expand Up @@ -106,6 +112,7 @@ class ConceptComponent(OssieObject):
identify_by: list[str] = Field(default_factory=list)
derived_by: list[str] = Field(default_factory=list)
requires: list[str] = Field(default_factory=list)
annotations: Annotations = Field(default_factory=dict)
relationships: list[Relationship] = Field(default_factory=list)


Expand Down Expand Up @@ -248,6 +255,7 @@ class OssieSpec(OssieObject):
name: str
description: str | None = None
prefixes: dict[str, str] = Field(default_factory=dict)
annotations: Annotations = Field(default_factory=dict)
requires: list[str] = Field(default_factory=list)
ai_context: AiContext | None = None
ontology: list[ConceptComponent] = Field(default_factory=list)
Expand Down
40 changes: 39 additions & 1 deletion converters/ontology/tests/test_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import pytest
import yaml

from ossie_ontology.spec import DatasetField, Metric, SemanticModel
from ossie_ontology.spec import DatasetField, Metric, OssieSpec, SemanticModel

# tests/ -> ontology -> converters -> <repo root>
_EXAMPLES_DIR = Path(__file__).resolve().parents[3] / "examples"
Expand Down Expand Up @@ -63,3 +63,41 @@ def test_tpcds_example_parses():
assert model.name == "tpcds_retail_model"
assert any(f.datatype for d in model.datasets for f in d.fields)
assert any(m.datatype for m in model.metrics)


def test_foaf_example_keeps_annotations():
"""`annotations` carries imported data the spec does not model, so it
must parse at every level that declares it and survive a dump unchanged."""
example_path = _EXAMPLES_DIR / "foaf_owl_import.yaml"
if not example_path.is_file():
pytest.skip(f"canonical example not present at {example_path}")

doc = yaml.safe_load(example_path.read_text(encoding="utf-8"))
spec = OssieSpec.model_validate(doc)

assert spec.annotations == {"dc:title": "Friend of a Friend (FOAF) vocabulary"}
person = spec.ontology[0]
assert person.annotations["owl:equivalentClass"] == "http://schema.org/Person"
assert person.relationships[0].annotations["rdfs:subPropertyOf"] == "rdfs:label"

dumped = OssieSpec.load_yaml(spec.dump_yaml()).dump_dict()
assert dumped["annotations"] == doc["annotations"]
assert dumped["ontology"][0]["annotations"] == doc["ontology"][0]["annotations"]
assert dumped["ontology"][0]["relationships"][1]["annotations"] == (
doc["ontology"][0]["relationships"][1]["annotations"]
)


def test_role_accepts_annotations():
relationship = OssieSpec.model_validate({
"name": "test",
"ontology": [{
"concept": "Person",
"type": "EntityType",
"relationships": [{
"name": "knows",
"roles": [{"concept": "Person", "name": "acquaintance", "annotations": {"source": "foaf"}}],
}],
}],
}).ontology[0].relationships[0]
assert relationship.roles[0].annotations == {"source": "foaf"}
65 changes: 65 additions & 0 deletions examples/foaf_owl_import.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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.

version: 0.2.0.dev0
name: foaf
description: >
Small ontology imported from the FOAF (Friend of a Friend) OWL vocabulary.
The importer writes RDF data that has a home in the spec to that field
(rdfs:comment becomes description) and preserves the rest (labels, axioms,
term status) in annotations, so the source can be round-tripped. This
importer keys annotations by the source predicate's qualified name;
that is the importer's convention, not a requirement of the spec.
prefixes:
foaf: "http://xmlns.com/foaf/0.1/"
rdfs: "http://www.w3.org/2000/01/rdf-schema#"
owl: "http://www.w3.org/2002/07/owl#"
dc: "http://purl.org/dc/elements/1.1/"
vs: "http://www.w3.org/2003/06/sw-vocab-status/ns#"
annotations:
dc:title: "Friend of a Friend (FOAF) vocabulary"
ontology:
- concept: Person
type: EntityType
description: "A person."
iri: foaf:Person
annotations:
rdfs:label: "Person"
owl:equivalentClass: "http://schema.org/Person"
vs:term_status: "stable"
relationships:
- name: name
description: "A name for some thing."
iri: foaf:name
roles:
- concept: String
verbalizes: [ "{Person} has name {String}" ]
multiplicity: ManyToOne
annotations:
rdfs:label: "name"
rdfs:subPropertyOf: "rdfs:label"
vs:term_status: "testing"
- name: knows
description: "A person known by this person (indicating some level of reciprocated interaction between the parties)."
iri: foaf:knows
roles:
- concept: Person
name: acquaintance
verbalizes: [ "{Person} knows {Person:acquaintance}" ]
annotations:
rdfs:label: "knows"
vs:term_status: "stable"
17 changes: 17 additions & 0 deletions ontology/ontology.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"$ref": "#/$defs/OntologyMap"
}
},
"annotations": {
"$ref": "#/$defs/Annotations"
},
"prefixes": {
"type": "object",
"description": "Maps namespace prefixes to the IRIs they abbreviate, enabling QName expansion (e.g., 'foaf' -> 'http://xmlns.com/foaf/0.1/').",
Expand All @@ -56,6 +59,11 @@
"required": ["version", "name", "ontology"],
"additionalProperties": false,
"$defs": {
"Annotations": {
"type": "object",
"description": "Open set of annotations for carrying data that is not covered by the core ontology spec, for example when importing an ontology from an external format such as OWL/RDF. Keys are free-form and values may be any JSON; the spec does not interpret their structure.",
"additionalProperties": true
},
"OntologyComponent": {
"type": "object",
"description": "Ontology component that defines a single concept and any relationships that are keyed primarily by that concept",
Expand Down Expand Up @@ -106,6 +114,9 @@
},
"description": "Defines relationships that pertain primarily to the concept defined in this component"
},
"annotations": {
"$ref": "#/$defs/Annotations"
},
"iri": {
"type": "string",
"description": "Optional global identifier for this concept, expressed as a full IRI or as a QName (prefix:local) resolved against the ontology-level 'prefixes' map."
Expand Down Expand Up @@ -161,6 +172,9 @@
},
"description": "Natural language expressions that verbalize this relationship"
},
"annotations": {
"$ref": "#/$defs/Annotations"
},
"iri": {
"type": "string",
"description": "Optional global identifier for this relationship, expressed as a full IRI or as a QName (prefix:local) resolved against the ontology-level 'prefixes' map."
Expand Down Expand Up @@ -232,6 +246,9 @@
"name": {
"type": "string",
"description": "Optional name of this role, used when the same concept plays multiple roles in the same relationship"
},
"annotations": {
"$ref": "#/$defs/Annotations"
}
},
"required": ["concept"],
Expand Down
45 changes: 45 additions & 0 deletions ontology/ontology.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ hierarchically, grouping each relationship under the concept that plays its firs
| `ontology` | list | Yes | Concepts and relationships they group that form this ontology |
| `prefixes` | object | No | Namespace prefixes used to abbreviate [IRIs](#global-identifiers) |
| `ontology_mappings` | list | No | Deprecated; accepted only so existing documents continue to validate. Write mappings as [mapping documents](#mapping-documents) instead |
| `annotations` | object | No | Open set of annotations for data not covered by this spec (see [Annotations](#annotations)) |

Each component of an ontology declares a concept and lists the relationships where that
concept plays the first role. The concept's name is the value of the `concept` field, and
Expand All @@ -114,6 +115,7 @@ Concepts have the following schema:
| `requires` | list | No | Expressions that constrain this concept's population |
| `relationships` | list | No | Relationships where this concept plays the first role |
| `iri` | string | No | Optional [global identifier](#global-identifiers) of this concept |
| `annotations` | object | No | Open set of annotations for data not covered by this spec (see [Annotations](#annotations)) |

Each concept is either an entity type or a value type.

Expand Down Expand Up @@ -159,6 +161,7 @@ Each relationship that is declared under a concept conforms to the following sch
| `requires` | list | No | Expressions that constrain this relationship's population |
| `verbalizes` | list | Yes | Patterns describing how to verbalize links |
| `iri` | string | No | Optional [global identifier](#global-identifiers) of this relationship |
| `annotations` | object | No | Open set of annotations for data not covered by this spec (see [Annotations](#annotations)) |

Each relationship is uniquely identified by prepending its declared name with that of the containing
concept. For instance, in:
Expand Down Expand Up @@ -202,6 +205,7 @@ using this schema:
|-------|------|----------|-------------|
| `concept` | string | Yes | Name of the concept that plays this role |
| `name` | string | No | Optional role name |
| `annotations` | object | No | Open set of annotations for data not covered by this spec (see [Annotations](#annotations)) |

For instance, in:

Expand Down Expand Up @@ -437,6 +441,45 @@ Expressions, roles, and mappings continue to use local names. The IRI serves too
between this specification and IRI-based languages and lets multiple ontologies state that they
refer to the same externally defined concept or relationship.

### Annotations

The ontology root, concepts, relationships, and roles may each carry an `annotations` object: an
open set of key-value pairs for data that the core spec does not model. This gives tools a place to
attach their own metadata, or to preserve information when importing an ontology from an external
format so that it can be round-tripped.

Keys are free-form, and values may be any JSON. The spec does not interpret or constrain the structure of
`annotations`; tools that do not understand a given annotation should preserve it as-is.

Use `annotations` only for data that the spec cannot express. Data that has a home in the spec,
such as a description, a supertype (`extends`), a derivation (`derived_by`), a constraint (`requires`),
or a verbalization (`verbalizes`), belongs in that field.

For example, an importer from the OWL version of the [FOAF](http://xmlns.com/foaf/0.1/) vocabulary
writes `rdfs:comment` to `description` and keeps the RDF data that has no home in the spec, keyed by
the source predicate's qualified name:

```yaml
prefixes:
foaf: "http://xmlns.com/foaf/0.1/"
rdfs: "http://www.w3.org/2000/01/rdf-schema#"
owl: "http://www.w3.org/2002/07/owl#"
vs: "http://www.w3.org/2003/06/sw-vocab-status/ns#"
ontology:
- concept: Person
type: EntityType
description: A person.
iri: foaf:Person
annotations:
rdfs:label: Person
owl:equivalentClass: http://schema.org/Person
vs:term_status: stable
```

The full document is in [`examples/foaf_owl_import.yaml`](../examples/foaf_owl_import.yaml). Keying
annotations by qualified name is this importer's convention. The choice of keys belongs to the tool that
writes them.

## Ontology mappings

Ontology mappings declare how to map the values of fields at the logical level to objects and links
Expand Down Expand Up @@ -695,6 +738,8 @@ though `Store` plays a role in three of the relationships.
- Optional IRIs on concepts and relationships, with namespace prefixes declared at the top level
- Mapping documents (`ontology/mapping.json`) that reference one ontology and one semantic
model; embedded `ontology_mappings` is deprecated
- `annotations` on the ontology root, concepts, relationships, and roles for carrying
data not covered by the core spec (e.g. when importing from OWL/RDF)

---

Expand Down
Loading