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
2 changes: 1 addition & 1 deletion examples/extract-classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/extract-ontology-simple.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions examples/extract-ontology.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand All @@ -19,7 +19,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand All @@ -30,7 +30,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions examples/extract-properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand All @@ -19,7 +19,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/generate-portal.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"endpoint": {
"@op": "URI",
"args": {
"input": "https://linkeddatahub.com/demo/skos/sparql"
"input": "https://unesco-thesaurus.demo.linkeddatahub.com/sparql"
}
},
"ontology_namespace": {
Expand Down
4 changes: 2 additions & 2 deletions formal-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Abstract: URI × Maybe URI → Any
Python: def execute(self, url: URIRef, block: URIRef = None) -> Any
```

**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
**ldh-GenerateOntologyViews** - Generate LDH views (`ldh:view`) and SPIN `sp:Select` queries for each non-`owl:FunctionalProperty` `owl:ObjectProperty` in an ontology graph; `owl:DatatypeProperty` is excluded because literal values are displayed inline in LDH and do not benefit from a table view
```
Abstract: Graph × URI × URI → Graph
Python: def execute(self, ontology: rdflib.Graph, base_uri: URIRef, service_uri: URIRef) -> rdflib.Graph
Expand Down Expand Up @@ -290,7 +290,7 @@ Abstract: URI → Graph
Python: def execute(self, endpoint: URIRef) -> rdflib.Graph
```

**ExtractObjectProperties** - Extract object properties from graph
**ExtractObjectProperties** - Extract object properties from instance data; infers `owl:FunctionalProperty` when global max objects-per-subject = 1 (closed-world assumption over present triples, ignores formal ontology at `/ns`)
```
Abstract: URI → Graph
Python: def execute(self, endpoint: URIRef) -> rdflib.Graph
Expand Down
113 changes: 113 additions & 0 deletions prompts/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,3 +962,116 @@ Result:
```json
"550e8400-e29b-41d4-a716-446655440000"
```

## PATCH(url: URL, update: str) -> Dict

Applies a SPARQL Update (DELETE/INSERT/WHERE) to a document via HTTP PATCH. Use this to atomically modify existing RDF triples in a LinkedDataHub document.

### Example JSON

```json
{
"@op": "PATCH",
"args": {
"url": "https://localhost:4443/corporations/",
"update": "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\nDELETE { <https://localhost:4443/corporations/> rdf:_1 ?m . ?m ?p ?o . }\nINSERT { <https://localhost:4443/corporations/> rdf:_1 _:m . _:m rdf:value <https://localhost:4443/corporations/#Instances_View> . }\nWHERE { <https://localhost:4443/corporations/> rdf:_1 ?m . ?m ?p ?o . }"
}
}
```

Result:
```json
{
"head": {"vars": ["status", "url"]},
"results": {
"bindings": [{
"status": {"type": "literal", "value": "200", "datatype": "http://www.w3.org/2001/XMLSchema#integer"},
"url": {"type": "uri", "value": "https://localhost:4443/corporations/"}
}]
}
}
```

## ExtractOntology(endpoint: str) -> Graph

Extracts a complete ontology (classes + datatype properties + object properties) from a SPARQL endpoint as a single merged graph. Infers structure from instance data using the closed-world assumption — does not rely on a formal ontology declaration at `/ns`. Properties where the global max objects-per-subject = 1 are emitted as `owl:FunctionalProperty`.

### Example JSON

```json
{
"@op": "ExtractOntology",
"args": {
"endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
```

Result: Returns a JSON-LD graph containing `owl:Class`, `owl:DatatypeProperty`, `owl:ObjectProperty`, and `owl:FunctionalProperty` declarations.

## ldh-GenerateOntologyViews(ontology: Graph, base_uri: URI, service_uri: URI) -> Graph

Generates LinkedDataHub view resources for each non-functional `owl:ObjectProperty` in an ontology graph. Produces `ldh:View` resources, `sp:Select` SPARQL queries, and `ldh:view` triples linking properties to their views. `owl:DatatypeProperty` and `owl:FunctionalProperty` are intentionally excluded.

### Example JSON

```json
{
"@op": "ldh-GenerateOntologyViews",
"args": {
"ontology": {
"@op": "ExtractOntology",
"args": {
"endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
},
"base_uri": "https://admin.localhost:4443/ontologies/namespace/",
"service_uri": "https://admin.localhost:4443/ontologies/namespace/#Service"
}
}
```

Result: Returns a JSON-LD graph containing `ldh:View`, `sp:Select`, and `ldh:view` triples.

## ldh-GenerateClassContainers(ontology: Graph, parent_container: URI, endpoint: URI) -> Dict

Creates a LinkedDataHub container for each `owl:Class` in an ontology graph. Each container gets a SPARQL service resource, an `sp:Select` instance-list query, an `ldh:View`, and a PATCH to wire the view as the container's primary (`rdf:_1`) view.

### Example JSON

```json
{
"@op": "ldh-GenerateClassContainers",
"args": {
"ontology": {
"@op": "ExtractOntology",
"args": {
"endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
},
"parent_container": "https://localhost:4443/",
"endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql"
}
}
```

Result: Returns a SPARQL results table with one binding per HTTP operation performed.

## ldh-GeneratePortal(endpoint: URI, ontology_namespace: URI, parent_container: URI) -> Dict

End-to-end portal generation: extracts an ontology from a SPARQL endpoint, generates views and posts them to the ontology namespace document, then creates class containers with instance views under the parent container. Composes `ExtractOntology`, `ldh-GenerateOntologyViews`, `POST`, and `ldh-GenerateClassContainers`.

### Example JSON

```json
{
"@op": "ldh-GeneratePortal",
"args": {
"endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql",
"ontology_namespace": "https://admin.localhost:4443/ontologies/namespace/",
"parent_container": "https://localhost:4443/"
}
}
```

Result: Returns a SPARQL results table aggregating all HTTP operation results.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from web_algebra.operation import Operation
from web_algebra.operations.linkeddatahub.create_container import CreateContainer
from web_algebra.operations.linked_data.post import POST
from web_algebra.operations.linked_data.patch import PATCH
from web_algebra.operations.linkeddatahub.add_generic_service import AddGenericService
from web_algebra.json_result import JSONResult

Expand Down Expand Up @@ -159,6 +160,29 @@ def execute(self, ontology: Graph, parent_container: URIRef, endpoint: URIRef) -
all_vars.update(post_view_result.vars)
logging.info(f"Posted view to {container_uri}")

# Step 5: Replace rdf:_1 (default ldh:ChildrenView) with the custom instances view.
patch_update = (
f"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n"
f"PREFIX ldh: <https://w3id.org/atomgraph/linkeddatahub#>\n\n"
f"DELETE {{\n"
f" <{container_uri}> rdf:_1 ?member .\n"
f" ?member ?p ?o .\n"
f"}}\nINSERT {{\n"
f" <{container_uri}> rdf:_1 _:member .\n"
f" _:member a ldh:Object ;\n"
f" rdf:value <{view_uri}> .\n"
f"}}\nWHERE {{\n"
f" <{container_uri}> rdf:_1 ?member .\n"
f" ?member ?p ?o .\n"
f"}}"
)
patch_result = PATCH(settings=self.settings, context=self.context).execute(
container_uri, Literal(patch_update, datatype=XSD.string)
)
all_bindings.extend(patch_result.bindings)
all_vars.update(patch_result.vars)
logging.info(f"Replaced rdf:_1 in {container_uri} with {view_uri}")

# Create concatenated Result using JSONResult
return JSONResult(list(all_vars), all_bindings)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

SELECT DISTINCT ?property ?propertyType
SELECT DISTINCT ?property
WHERE {
?property a ?propertyType .
FILTER(?propertyType IN (owl:DatatypeProperty, owl:ObjectProperty))
?property a owl:ObjectProperty .
FILTER NOT EXISTS { ?property a owl:FunctionalProperty }
}
ORDER BY ?property
Expand All @@ -108,12 +107,9 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra
for row in results:
row_dict = row.asdict()
property_uri = row_dict["property"]
property_type = row_dict["propertyType"]

if not isinstance(property_uri, URIRef):
raise TypeError(f"Expected property to be URIRef, got {type(property_uri)}")
if not isinstance(property_type, URIRef):
raise TypeError(f"Expected propertyType to be URIRef, got {type(property_type)}")

# Disambiguate when two properties share a local name (different namespaces).
property_local = self._get_local_name(property_uri)
Expand Down
47 changes: 8 additions & 39 deletions src/web_algebra/operations/schema/extract_object_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ def execute(self, endpoint: URIRef) -> Graph:
"""Pure function: extract OWL object properties with RDFLib terms

Infers functional properties using closed world assumption:
- Counts max cardinality by examining all subjects in the dataset
- Creates OWL restriction with maxQualifiedCardinality
- When maxC = 1, property is inferred to be functional in this dataset
- Counts max cardinality per property across all subjects in the dataset
- When global max = 1, emits ?property a owl:FunctionalProperty
- Note: Inference based solely on present data, not formal ontology definitions
"""
query = Literal("""
Expand All @@ -32,71 +31,50 @@ def execute(self, endpoint: URIRef) -> Graph:
PREFIX owl: <http://www.w3.org/2002/07/owl#>

CONSTRUCT {
# Basic property metadata - constructed for ALL properties
?property a owl:ObjectProperty ;
rdfs:domain ?domain ;
rdfs:range ?range .

# Restriction triples - only constructed when ?restriction and ?maxCardinality are bound
# This happens only for functional properties (maxC = 1)
?domain rdfs:subClassOf ?restriction .

?restriction a owl:Restriction ;
owl:onProperty ?property ;
owl:maxQualifiedCardinality ?maxCardinality ;
owl:onClass ?range .
rdfs:domain ?domain ;
rdfs:range ?range .
?functional a owl:FunctionalProperty .
}
WHERE {
{
# Outermost SELECT: Conditionally bind maxCardinality only when maxC = 1
# The IF expression makes ?maxCardinality unbound for non-functional properties
SELECT ?domain ?property ?range (IF(?maxC = 1, ?maxC, ?UNDEF) AS ?maxCardinality)
SELECT ?property ?domain ?range (IF(?maxC = 1, ?property, ?UNDEF) AS ?functional)
WHERE {
{
# Outer SELECT: Aggregate to single domain/range per property
# Filter to properties with unambiguous range (COUNT DISTINCT <= 1)
# Get final max cardinality value
SELECT ?property (SAMPLE(?d) AS ?domain) (SAMPLE(?r) AS ?range) (MAX(?maxC2) AS ?maxC)
WHERE {
{
# Middle SELECT: Get max cardinality per property-range pair
# Filter to properties with unambiguous domain (COUNT DISTINCT <= 1)
SELECT ?property ?r (SAMPLE(?d2) AS ?d) (MAX(?c) AS ?maxC2)
WHERE {
{
# Innermost SELECT: Count objects per subject-property-range triple
# This gives us cardinality for each individual subject
SELECT ?subject ?property ?r (COUNT(?object) AS ?c)
WHERE {
{
?subject ?property ?object .
FILTER(?property != rdf:type)
FILTER(!isLiteral(?object))

OPTIONAL {
{ ?object a ?r }
UNION
{ GRAPH ?objG { ?object a ?r } }
FILTER (!isBlank(?r))
FILTER(!isBlank(?r))
}
} UNION {
GRAPH ?g {
?subject ?property ?object .
FILTER(?property != rdf:type)
FILTER(!isLiteral(?object))

OPTIONAL {
{ ?object a ?r }
UNION
{ GRAPH ?objG { ?object a ?r } }
FILTER (!isBlank(?r))
FILTER(!isBlank(?r))
}
}
}
}
GROUP BY ?subject ?property ?r
}

OPTIONAL {
{ ?subject a ?d2 }
UNION
Expand All @@ -113,15 +91,6 @@ def execute(self, endpoint: URIRef) -> Graph:
}
}
}

# Create blank node for restriction ONLY when ?maxCardinality is bound (functional properties)
# This OPTIONAL block does NOT filter out rows - all properties are still returned
# For functional properties: ?restriction is bound, and restriction triples are constructed
# For non-functional properties: ?restriction is unbound, no restriction triples created
OPTIONAL {
FILTER(BOUND(?maxCardinality))
BIND(BNODE() AS ?restriction)
}
}
""", datatype=XSD.string)
return super().execute(endpoint, query)
Expand Down
Loading