Skip to content

Commit 4d027de

Browse files
authored
Merge pull request #20 from AtomGraph/feat-spec-portal-ops
Spec and unit tests for portal/ontology operations
2 parents c714b79 + 2fc83a1 commit 4d027de

9 files changed

Lines changed: 270 additions & 0 deletions

formal-semantics.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@ Abstract: URI × Maybe URI → Any
258258
Python: def execute(self, url: URIRef, block: URIRef = None) -> Any
259259
```
260260

261+
**ldh-GenerateOntologyViews** - Generate LDH views (`ldh:view`) and SPIN `sp:Select` queries for each non-`owl:FunctionalProperty` `owl:DatatypeProperty`/`owl:ObjectProperty` in an ontology graph
262+
```
263+
Abstract: Graph × URI × URI → Graph
264+
Python: def execute(self, ontology: rdflib.Graph, base_uri: URIRef, service_uri: URIRef) -> rdflib.Graph
265+
```
266+
267+
**ldh-GenerateClassContainers** - Create an LDH container per `owl:Class` in an ontology graph (each with a SPARQL service and instance-list view)
268+
```
269+
Abstract: Graph × URI × URI → Result
270+
Python: def execute(self, ontology: rdflib.Graph, parent_container: URIRef, endpoint: URIRef) -> Result
271+
```
272+
273+
**ldh-GeneratePortal** - End-to-end portal generation; composes `ExtractOntology`, `ldh-GenerateOntologyViews`, `POST`, and `ldh-GenerateClassContainers`
274+
```
275+
Abstract: URI × URI × URI → Result
276+
Python: def execute(self, endpoint: URIRef, ontology_namespace: URIRef, parent_container: URIRef) -> Result
277+
```
278+
261279
### Schema Operations
262280

263281
**ExtractClasses** - Extract RDF classes from graph
@@ -278,6 +296,12 @@ Abstract: URI → Graph
278296
Python: def execute(self, endpoint: URIRef) -> rdflib.Graph
279297
```
280298

299+
**ExtractOntology** - Extract a full ontology (classes + datatype + object properties) from a SPARQL endpoint as a single graph
300+
```
301+
Abstract: URI → Graph
302+
Python: def execute(self, endpoint: URIRef) -> rdflib.Graph
303+
```
304+
281305
### Utility Operations
282306

283307
**Merge** - Merge multiple RDF graphs into one

src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ def execute(self, ontology: Graph, parent_container: URIRef, endpoint: URIRef) -
6060
Returns:
6161
Concatenated Result containing all operation results (CreateContainer + AddGenericService + POST bindings)
6262
"""
63+
if not isinstance(ontology, Graph):
64+
raise TypeError(
65+
f"GenerateClassContainers operation expects 'ontology' to be Graph, got {type(ontology)}"
66+
)
67+
if not isinstance(parent_container, URIRef):
68+
raise TypeError(
69+
f"GenerateClassContainers operation expects 'parent_container' to be URIRef, got {type(parent_container)}"
70+
)
71+
if not isinstance(endpoint, URIRef):
72+
raise TypeError(
73+
f"GenerateClassContainers operation expects 'endpoint' to be URIRef, got {type(endpoint)}"
74+
)
6375
# Define namespaces
6476
LDH = Namespace("https://w3id.org/atomgraph/linkeddatahub#")
6577
SP = Namespace("http://spinrdf.org/sp#")

src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra
5656
Returns:
5757
RDF graph containing ldh:View, sp:Select, and ldh:view triples
5858
"""
59+
if not isinstance(ontology, Graph):
60+
raise TypeError(
61+
f"GenerateOntologyViews operation expects 'ontology' to be Graph, got {type(ontology)}"
62+
)
63+
if not isinstance(base_uri, URIRef):
64+
raise TypeError(
65+
f"GenerateOntologyViews operation expects 'base_uri' to be URIRef, got {type(base_uri)}"
66+
)
67+
if not isinstance(service_uri, URIRef):
68+
raise TypeError(
69+
f"GenerateOntologyViews operation expects 'service_uri' to be URIRef, got {type(service_uri)}"
70+
)
71+
5972
# Define namespaces
6073
LDH = Namespace("https://w3id.org/atomgraph/linkeddatahub#")
6174
SP = Namespace("http://spinrdf.org/sp#")

src/web_algebra/operations/linkeddatahub/content/generate_portal.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ def execute(self, endpoint: URIRef, ontology_namespace: URIRef, parent_container
5151
Returns:
5252
Concatenated Result containing all operation results
5353
"""
54+
if not isinstance(endpoint, URIRef):
55+
raise TypeError(
56+
f"GeneratePortal operation expects 'endpoint' to be URIRef, got {type(endpoint)}"
57+
)
58+
if not isinstance(ontology_namespace, URIRef):
59+
raise TypeError(
60+
f"GeneratePortal operation expects 'ontology_namespace' to be URIRef, got {type(ontology_namespace)}"
61+
)
62+
if not isinstance(parent_container, URIRef):
63+
raise TypeError(
64+
f"GeneratePortal operation expects 'parent_container' to be URIRef, got {type(parent_container)}"
65+
)
66+
5467
import logging
5568

5669
# Step 0: Create service resource for the SPARQL endpoint

src/web_algebra/operations/schema/extract_ontology.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def inputSchema(cls) -> dict:
2727

2828
def execute(self, endpoint: URIRef) -> Graph:
2929
"""Extract complete ontology by composing individual extraction operations"""
30+
if not isinstance(endpoint, URIRef):
31+
raise TypeError(
32+
f"ExtractOntology operation expects 'endpoint' to be URIRef, got {type(endpoint)}"
33+
)
3034

3135
# Extract classes
3236
classes_graph = ExtractClasses(settings=self.settings, context=self.context).execute(endpoint)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Spec: formal-semantics.md "ExtractOntology - Extract a full ontology (classes
2+
+ datatype + object properties) from a SPARQL endpoint as a single graph"
3+
Abstract: URI → Graph
4+
Python: def execute(self, endpoint: URIRef) -> rdflib.Graph
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import pytest
10+
from rdflib import Literal
11+
12+
from web_algebra.operation import Operation
13+
14+
15+
class TestExtractOntologyPure:
16+
def test_wrong_input_type_raises(self, settings):
17+
op = Operation.get("ExtractOntology")(settings=settings)
18+
with pytest.raises(TypeError):
19+
op.execute(Literal("not-a-uri"))
20+
21+
@pytest.mark.skip(reason="UNCLEAR(spec): is the URI a SPARQL endpoint, document URL, or ontology IRI? — narrative omits this")
22+
def test_happy_path(self, settings):
23+
pass
24+
25+
26+
class TestExtractOntologyJson:
27+
@pytest.mark.skip(reason="UNCLEAR(spec): JSON arg key for ExtractOntology not given by spec or existing fixtures")
28+
def test_json_dispatch(self, settings):
29+
pass
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Spec: formal-semantics.md "ldh-GenerateClassContainers - Create an LDH
2+
container per `owl:Class` in an ontology graph (each with a SPARQL service and
3+
instance-list view)"
4+
Abstract: Graph × URI × URI → Result
5+
Python: def execute(self, ontology: rdflib.Graph, parent_container: URIRef, endpoint: URIRef) -> Result
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import pytest
11+
from rdflib import Graph, Literal, URIRef
12+
13+
from web_algebra.operation import Operation
14+
15+
16+
PARENT = URIRef("http://example.org/portal/")
17+
ENDPOINT = URIRef("http://example.org/sparql")
18+
19+
20+
class TestLDHGenerateClassContainersPure:
21+
def test_wrong_ontology_type_raises(self, settings):
22+
op = Operation.get("ldh-GenerateClassContainers")(settings=settings)
23+
with pytest.raises(TypeError):
24+
op.execute(Literal("not-a-graph"), PARENT, ENDPOINT)
25+
26+
def test_wrong_parent_container_type_raises(self, settings):
27+
op = Operation.get("ldh-GenerateClassContainers")(settings=settings)
28+
with pytest.raises(TypeError):
29+
op.execute(Graph(), Literal("not-a-uri"), ENDPOINT)
30+
31+
def test_wrong_endpoint_type_raises(self, settings):
32+
op = Operation.get("ldh-GenerateClassContainers")(settings=settings)
33+
with pytest.raises(TypeError):
34+
op.execute(Graph(), PARENT, Literal("not-a-uri"))
35+
36+
37+
@pytest.mark.ldh
38+
class TestLDHGenerateClassContainersLive:
39+
@pytest.mark.skip(reason="UNCLEAR(spec): return type `Result` shape — what's a meaningful assertion for a side-effecting orchestration?")
40+
def test_basic(self, settings_with_auth):
41+
pass
42+
43+
44+
class TestLDHGenerateClassContainersJson:
45+
@pytest.mark.skip(reason="UNCLEAR(spec): JSON arg keys for ldh-GenerateClassContainers not given by spec or existing fixtures")
46+
def test_json_dispatch(self, settings):
47+
pass
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Spec: formal-semantics.md "ldh-GenerateOntologyViews - Generate LDH views
2+
(`ldh:view`) and SPIN `sp:Select` queries for each non-`owl:FunctionalProperty`
3+
`owl:DatatypeProperty`/`owl:ObjectProperty` in an ontology graph"
4+
Abstract: Graph × URI × URI → Graph
5+
Python: def execute(self, ontology: rdflib.Graph, base_uri: URIRef, service_uri: URIRef) -> rdflib.Graph
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import pytest
11+
from rdflib import Graph, Literal, Namespace, URIRef
12+
from rdflib.namespace import OWL, RDF
13+
14+
from web_algebra.operation import Operation
15+
16+
17+
LDH = Namespace("https://w3id.org/atomgraph/linkeddatahub#")
18+
EX = Namespace("http://example.org/ns#")
19+
BASE = URIRef("http://example.org/portal/")
20+
SERVICE = URIRef("http://example.org/portal/#Service")
21+
22+
23+
class TestLDHGenerateOntologyViewsPure:
24+
def test_wrong_ontology_type_raises(self, settings):
25+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
26+
with pytest.raises(TypeError):
27+
op.execute(Literal("not-a-graph"), BASE, SERVICE)
28+
29+
def test_wrong_base_uri_type_raises(self, settings):
30+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
31+
with pytest.raises(TypeError):
32+
op.execute(Graph(), Literal("not-a-uri"), SERVICE)
33+
34+
def test_wrong_service_uri_type_raises(self, settings):
35+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
36+
with pytest.raises(TypeError):
37+
op.execute(Graph(), BASE, Literal("not-a-uri"))
38+
39+
def test_emits_ldh_view_for_non_functional_property(self, settings):
40+
"""Spec: emits a view per non-`owl:FunctionalProperty` object/datatype property."""
41+
ontology = Graph()
42+
ontology.add((EX.knows, RDF.type, OWL.ObjectProperty))
43+
44+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
45+
out = op.execute(ontology, BASE, SERVICE)
46+
47+
assert isinstance(out, Graph)
48+
views = list(out.triples((EX.knows, LDH.view, None)))
49+
assert len(views) == 1, f"expected one ldh:view triple for ex:knows, got {len(views)}"
50+
51+
def test_skips_functional_property(self, settings):
52+
"""Spec: properties declared `owl:FunctionalProperty` are excluded."""
53+
ontology = Graph()
54+
ontology.add((EX.ssn, RDF.type, OWL.DatatypeProperty))
55+
ontology.add((EX.ssn, RDF.type, OWL.FunctionalProperty))
56+
57+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
58+
out = op.execute(ontology, BASE, SERVICE)
59+
60+
views = list(out.triples((EX.ssn, LDH.view, None)))
61+
assert len(views) == 0, "functional property must not get a view"
62+
63+
def test_no_ldh_template_in_output(self, settings):
64+
"""Spec phrases the output predicate as `ldh:view` — `ldh:template` (the
65+
previous LDH vocabulary, since removed) must not appear."""
66+
ontology = Graph()
67+
ontology.add((EX.knows, RDF.type, OWL.ObjectProperty))
68+
ontology.add((EX.name, RDF.type, OWL.DatatypeProperty))
69+
70+
op = Operation.get("ldh-GenerateOntologyViews")(settings=settings)
71+
out = op.execute(ontology, BASE, SERVICE)
72+
73+
legacy = list(out.triples((None, LDH.template, None)))
74+
assert legacy == [], "output must contain no `ldh:template` triples"
75+
76+
77+
class TestLDHGenerateOntologyViewsJson:
78+
@pytest.mark.skip(reason="UNCLEAR(spec): JSON arg keys for ldh-GenerateOntologyViews not given by spec or existing fixtures")
79+
def test_json_dispatch(self, settings):
80+
pass
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Spec: formal-semantics.md "ldh-GeneratePortal - End-to-end portal generation;
2+
composes `ExtractOntology`, `ldh-GenerateOntologyViews`, `POST`, and
3+
`ldh-GenerateClassContainers`"
4+
Abstract: URI × URI × URI → Result
5+
Python: def execute(self, endpoint: URIRef, ontology_namespace: URIRef, parent_container: URIRef) -> Result
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import pytest
11+
from rdflib import Literal, URIRef
12+
13+
from web_algebra.operation import Operation
14+
15+
16+
ENDPOINT = URIRef("http://example.org/sparql")
17+
ONTOLOGY_NS = URIRef("http://example.org/ontology/")
18+
PARENT = URIRef("http://example.org/portal/")
19+
20+
21+
class TestLDHGeneratePortalPure:
22+
def test_wrong_endpoint_type_raises(self, settings):
23+
op = Operation.get("ldh-GeneratePortal")(settings=settings)
24+
with pytest.raises(TypeError):
25+
op.execute(Literal("not-a-uri"), ONTOLOGY_NS, PARENT)
26+
27+
def test_wrong_ontology_namespace_type_raises(self, settings):
28+
op = Operation.get("ldh-GeneratePortal")(settings=settings)
29+
with pytest.raises(TypeError):
30+
op.execute(ENDPOINT, Literal("not-a-uri"), PARENT)
31+
32+
def test_wrong_parent_container_type_raises(self, settings):
33+
op = Operation.get("ldh-GeneratePortal")(settings=settings)
34+
with pytest.raises(TypeError):
35+
op.execute(ENDPOINT, ONTOLOGY_NS, Literal("not-a-uri"))
36+
37+
38+
@pytest.mark.ldh
39+
class TestLDHGeneratePortalLive:
40+
@pytest.mark.skip(reason="UNCLEAR(spec): return type `Result` shape — what's a meaningful assertion for end-to-end portal generation?")
41+
def test_basic(self, settings_with_auth):
42+
pass
43+
44+
45+
class TestLDHGeneratePortalJson:
46+
@pytest.mark.skip(reason="UNCLEAR(spec): JSON arg keys for ldh-GeneratePortal not given by spec or existing fixtures")
47+
def test_json_dispatch(self, settings):
48+
pass

0 commit comments

Comments
 (0)