Successfully emitting valid (!) ro-crate

This commit is contained in:
Simone Baffelli
2025-08-18 15:27:10 +02:00
parent 8b02b21654
commit ec2a182aca
8 changed files with 243 additions and 139 deletions
@@ -2,24 +2,46 @@ from lib_ro_crate_schema.crate.ro import RO_ID_LITERAL
from pydantic import BaseModel, Field
from rdflib.graph import Node
from rdflib import URIRef, RDF, Literal
from lib_ro_crate_schema.crate.rdf import is_type
from lib_ro_crate_schema.crate.rdf import is_type, object_id
from typing import Union
from lib_ro_crate_schema.crate.type_property import TypeProperty
from lib_ro_crate_schema.crate.type import Type
class MetadataEntry(BaseModel):
id: str
props: dict[str, str]
references: dict[str, list[str]] | None = None
id: str
# props: property reference (TypeProperty or str) -> value
props: dict[Union[TypeProperty, str], str]
#Types can be either strings or directly references to Type (RDF Types)
types: list[Union[Type, str]]
# references: property reference (TypeProperty or str) -> list of type references (Type or str)
references: dict[Union[TypeProperty, str], list[Union[Type, str]]] | None = None
children_identifiers: list[str] | None = None
parent_identifiers: list[str] | None = None
def to_triples(self, subject=None):
subj = URIRef(self.id) if subject is None else subject
if self.types:
for t in self.types:
yield is_type(self.id, URIRef(t))
if self.props:
for p, v in self.props.items():
yield (subj, URIRef(p), Literal(v))
if self.references:
for p, vs in self.references.items():
for v in vs:
yield (subj, URIRef(p), URIRef(v))
def to_triples(self):
subj = object_id(self.id)
for current_type in self.types:
match current_type:
case str(tid):
yield is_type(self.id, URIRef(tid))
case Type(id=tid):
yield is_type(self.id, URIRef(tid))
for prop_name, prop_value in self.props.items():
yield (subj, object_id(prop_name), Literal(prop_value))
# # If you have a types field, emit type triples (optional, not in original fields)
# if hasattr(self, 'types') and self.types:
# for t in self.types:
# tid = t.id if hasattr(t, 'id') else t
# yield is_type(self.id, URIRef(tid))
# if self.props:
# for p, v in self.props.items():
# pid = p.id if hasattr(p, 'id') else p
# yield (subj, URIRef(pid), Literal(v))
# if self.references:
# for p, vs in self.references.items():
# pid = p.id if hasattr(p, 'id') else p
# for v in vs:
# vid = v.id if hasattr(v, 'id') else v
# yield (subj, URIRef(pid), URIRef(vid))
@@ -7,8 +7,8 @@ from lib_ro_crate_schema.crate.ro_constants import (
OWL_MIN_CARDINALITY,
)
from pydantic import BaseModel, Field
from rdflib import URIRef, RDF, OWL, Literal
from lib_ro_crate_schema.crate.rdf import is_type
from rdflib import URIRef, RDF, OWL, Literal, URIRef
from lib_ro_crate_schema.crate.rdf import is_type, object_id
class OwlRestriction(BaseModel):
@@ -18,7 +18,7 @@ class OwlRestriction(BaseModel):
max_cardinality: TLiteral[0, 1]
def to_triples(self, subject=None):
subj = URIRef(self.id) if subject is None else subject
subj = object_id(self.id) if subject is None else subject
yield is_type(self.id, URIRef(OWL_RESTRICTION))
yield (subj, URIRef(ON_PROPERTY), URIRef(self.on_property))
yield (subj, URIRef(OWL_MIN_CARDINALITY), Literal(self.min_cardinality))
@@ -1,4 +1,4 @@
from typing import List, Literal
from typing import Generator, List, Literal
from lib_ro_crate_schema.crate.ro_constants import RDFS_SUBCLASS_OF
from lib_ro_crate_schema.crate.type_property import TypeProperty, RoTypeProperty
from lib_ro_crate_schema.crate.ro import (
@@ -10,7 +10,7 @@ from lib_ro_crate_schema.crate.ro import (
)
from pydantic import BaseModel, Field
from rdflib import URIRef, RDF, RDFS, Literal, Node
from lib_ro_crate_schema.crate.rdf import is_type
from lib_ro_crate_schema.crate.rdf import Triple, is_type
class RdfsClass(RoEntity):
@@ -24,7 +24,7 @@ class RdfsClass(RoEntity):
)
rdfs_properties: List[RoTypeProperty] | None = None
def to_triples(self, subject=None):
def to_triples(self, subject=None) -> Generator[Triple]:
subj = URIRef(self.id) if subject is None else subject
yield is_type(self.id, RDFS.Class)
if self.subclass_of:
@@ -1,5 +1,6 @@
from typing import Literal
from typing import Literal as TLiteral
from lib_ro_crate_schema.crate.owl_restriction import OwlRestriction
from lib_ro_crate_schema.crate.rdf import is_type, object_id
from lib_ro_crate_schema.crate.ro import ToRo
from lib_ro_crate_schema.crate.ro_constants import (
OWL_MIN_CARDINALITY,
@@ -7,27 +8,26 @@ from lib_ro_crate_schema.crate.ro_constants import (
OWL_RESTRICTION,
)
from pydantic import BaseModel, Field
from rdflib import URIRef, OWL, Literal, XSD
from .type_property import TypeProperty
from uuid import uuid4
class Restriction(BaseModel):
id: str
id: str = f"{uuid4()}"
property_type: str
min_cardinality: Literal[0, 1]
max_cardinality: Literal[0, 1]
min_cardinality: TLiteral[0, 1]
max_cardinality: TLiteral[0, 1]
class Config:
validate_by_name = True
populate_by_name = True
def to_ro(self):
return OwlRestriction(
id=self.id,
self_type=OWL_RESTRICTION,
on_property=self.property_type,
min_cardinality=self.min_cardinality,
max_cardinality=self.max_cardinality,
)
def to_triples(self, subject=None):
yield from self.to_ro().to_triples(subject=subject)
def to_triples(self):
subj = object_id(self.id)
yield is_type(self.id, OWL.Restriction)
yield (subj, OWL.onProperty, object_id(self.property_type))
yield (subj, OWL.minCardinality, Literal(self.min_cardinality, datatype=XSD.integer))
yield (subj, OWL.maxCardinality, Literal(self.max_cardinality,datatype=XSD.integer))
@@ -1,13 +1,55 @@
# Constants from Java SchemaFacade
from typing import Literal
from collections import defaultdict
from typing import Generator, Literal
from lib_ro_crate_schema.crate.metadata_entry import MetadataEntry
from lib_ro_crate_schema.crate.rdf import Triple, object_id
from lib_ro_crate_schema.crate.type import Type
from lib_ro_crate_schema.crate.type_property import TypeProperty
from pydantic import BaseModel
from lib_ro_crate_schema.crate.rdf import SCHEMA
from rdflib import RDFS, RDF
type TypeRegistry = dict[TypeProperty, list[Type]]
from typing import List, Tuple
type TypeRegistry = List[Tuple[TypeProperty, Type]]
def types_to_triples(used_types: TypeRegistry) -> Generator[Triple, None, None]:
"""
Emits all the triples
that represent the types found in the TypeRegistry
Each key is a TypeProperty, each value is a list of Type objects using it.
"""
for property_obj, cls in used_types:
prop_with_domain = property_obj.model_copy(update=dict(domain_includes=[cls.id]))
yield from prop_with_domain.to_triples()
class SchemaFacade(BaseModel):
types: list[Type]
properties: list[TypeProperty]
metadata_entries: list[MetadataEntry]
def collect_properties(self) -> TypeRegistry:
"""
Creates a registry of RDFS properties used in all RDFS classes.
Maps TypeProperty objects to the list of Type objects using them.
"""
result: List[Tuple[TypeProperty, Type]] = []
for cls in self.types:
for prop in getattr(cls, 'rdfs_property', []):
result.append((prop, cls))
return result
def to_triples(self) -> Generator[Triple, None, None]:
registry = self.collect_properties()
yield from types_to_triples(registry)
for p in self.types:
yield from p.to_triples()
for m in self.metadata_entries:
yield from m.to_triples()
@@ -1,4 +1,4 @@
from typing import List, Optional, Union
from typing import List, Optional, Union, Generator
from lib_ro_crate_schema.crate.rdf import is_type, object_id
from lib_ro_crate_schema.crate.rdfs_class import RdfsClass
@@ -10,42 +10,52 @@ from pydantic import BaseModel
from rdflib import Node, Literal, URIRef, RDF, RDFS, XSD, IdentifiedNode, OWL
class Type(BaseModel):
id: str
id: str
type: str
subclass_of: List[str] | None
ontological_annotations: List[str] | None
rdfs_property: List[TypeProperty] | None
comment: str
label: str
restrictions: List[Restriction] | None
def to_triples(self) -> list[Node]:
def get_restrictions(self) -> list[Restriction]:
"""
Get the restrictions that
represent the properties of this type (RDFS:Class)
"""
return [
Restriction(property_type=prop.id, min_cardinality=1, max_cardinality=1)
for prop in self.rdfs_property
if self.rdfs_property
]
def to_triples(self) -> Generator[Node]:
"""
Emits the type definition as a set of triples
whose subject is a RDFS:Class
"""
yield is_type(self.id, RDFS.Class)
yield (object_id(self.id), RDFS.comment, Literal(self.comment))
yield (object_id(self.id), RDFS.label, Literal(self.label))
annotations = [(object_id(self.id), OWL.equivalentClass, URIRef(cls)) for cls in self.ontological_annotations]
annotations = [
(object_id(self.id), OWL.equivalentClass, URIRef(cls))
for cls in self.ontological_annotations
]
for ann in annotations:
yield ann
for prop in self.rdfs_property:
for prop in self.get_restrictions():
print(prop)
yield from prop.to_triples()
# def to_ro(self) -> RdfsClass:
# return RdfsClass(id=self.id,
# self_type="rdfs:Class",
# return RdfsClass(id=self.id,
# self_type="rdfs:Class",
# subclass_of=serialize_references(self.subclass_of),
# #rdfs_properties=[prop.to_ro() for prop in self.rdfs_property] if self.rdfs_property is not None else None,
# ontological_annotations=None)
# def to_ro(self):
# return RdfsClass(
# id=RoId(id=self.id),
@@ -37,7 +37,7 @@ class TypeProperty(BaseModel):
yield (subj, RDFS.comment, Literal(self.comment))
if self.domain_includes:
for d in self.domain_includes:
yield (subj, URIRef(DOMAIN_IDENTIFIER), URIRef(d))
yield (subj, SCHEMA.domainIncludes, URIRef(d))
if self.range_includes:
for r in self.range_includes:
yield (subj, SCHEMA.rangeIncludes, URIRef(r))
@@ -1,4 +1,6 @@
from lib_ro_crate_schema.crate.prefix import extract_uses_namespaces
from pathlib import Path
import tempfile
import json
from lib_ro_crate_schema.crate.rdf import BASE, SCHEMA, unbind
from lib_ro_crate_schema.crate.type import Type, RdfsClass
from lib_ro_crate_schema.crate.type_property import TypeProperty, RoTypeProperty
@@ -10,104 +12,132 @@ from rocrate.model import ContextEntity
from rdflib import Graph, RDF
import pyld
RO_EXTRA_CTX = {
"owl:minCardinality": {"@type": "xsd:integer"},
"owl:maxCardinality": {"@type": "xsd:integer"},
}
def update_jsonld_context(ld_obj: dict, new_context: dict[str, str]) -> dict:
"""
Use pyld to update the @context of a JSON-LD object.
Returns a new JSON-LD object with the updated context.
"""
return pyld.jsonld.compact(ld_obj, new_context)
def get_context(g: Graph) -> dict[str, str]:
"""
Extracts all used namespaces from the rdflib graph and returns a JSON-LD @context dict.
This can be used for JSON-LD compaction or as a base for RO-Crate @context.
"""
# Get all namespaces used in the graph
context = {}
for prefix, namespace in g.namespaces():
# Avoid default empty prefix
if prefix:
context[prefix] = str(namespace)
# Optionally, add schema.org and other common ones if not present
if "schema" not in context:
context["schema"] = "https://schema.org/"
return context
def emit_crate_with_context(crate: ROCrate, context: dict) -> dict:
"""
Emits the ROCrate to a temporary file, reads it back, updates the @context directly (no pyld),
and returns the updated JSON-LD dict. Uses the tempfile context manager for cleanup.
"""
with tempfile.TemporaryDirectory() as tmp:
crate.metadata.write(tmp)
ld = json.loads((Path(tmp) / Path("ro-crate-metadata.json")).read_text())
# Only allow old context as string (RO-Crate style), else raise error
orig_ctx = ld.get("@context")
if isinstance(orig_ctx, str):
ld["@context"] = [orig_ctx, context]
else:
raise ValueError(
f"Unsupported original @context type: {type(orig_ctx)}. Only string is supported for RO-Crate compatibility."
)
return ld
return pyld.jsonld.compact(ld, context)
def add_schema_to_crate(schema: SchemaFacade, crate: ROCrate) -> dict:
"""
Emits triples from schema, builds a graph, compacts JSON-LD, adds objects to the crate,
writes to a tempfile, updates context using pyld, and returns the final JSON-LD dict.
"""
triples = schema.to_triples()
metadata_graph = Graph()
metadata_graph.bind("base", BASE)
for t in triples:
metadata_graph.add(t)
# Serialize and compact JSON-LD
ld_ser = metadata_graph.serialize(format="json-ld")
ld_obj = pyld.jsonld.json.loads(ld_ser)
context = {**get_context(metadata_graph), **RO_EXTRA_CTX}
ld_obj_compact = update_jsonld_context(ld_obj, context)
# Add each object in the compacted graph to the crate
for obj in ld_obj_compact.get("@graph", []):
crate.add_jsonld(obj)
# Use the tempfile-based utility to update context and return
return emit_crate_with_context(crate, context)
def main():
has_name = TypeProperty(id="hasName", range_includes_data_type=[LiteralType.STRING])
has_identifier = TypeProperty(
id="hasIdentifier", range_includes_data_type=[LiteralType.STRING]
)
rdfs_type = Type(
creator_type = Type(
id="Creator",
type="Type",
subclass_of=["https://schema.org/Thing"],
ontological_annotations=[
"http://purl.org/dc/terms/creator"
],
rdfs_property=[has_name],
ontological_annotations=["http://purl.org/dc/terms/creator"],
rdfs_property=[has_name, has_identifier],
comment="",
label="",
restrictions=None,
)
triples = rdfs_type.to_triples()
g = Graph()
g.bind("base", BASE)
tps = [g.add(t) for t in triples]
# g1 = unbind(g)
context = {str(label):str(value) for label, value in g.namespace_manager.namespaces()}
ser = g.serialize(format="json-ld", indent=4)
print(context)
ld = pyld.jsonld.json.loads(ser)
breakpoint()
compacted = pyld.jsonld.compact(ld, context)
print(pyld.jsonld.json.dumps(compacted))
# facade = SchemaFacade(types=[rdfs_type], metadata_entries=[])
# Example MetadataEntry using property and type references (object and string)
creator_entry = MetadataEntry(
id="creator1",
types=[creator_type],
props={
"has_name": "John Author",
"has_identifier": "https://orcid.org/0000-0000-0000-0000",
},
references={},
)
# Example with string property references (for flexibility)
creator_entry_str = MetadataEntry(
id="creator2",
types=[creator_type],
props={
"hasName": "Jane Author",
"hasIdentifier": "https://orcid.org/0000-0000-0000-0001",
},
references={},
)
schema = SchemaFacade(
types=[creator_type],
properties=[has_name, has_identifier],
metadata_entries=[creator_entry, creator_entry_str],
)
crate = ROCrate()
crate.license = "a"
crate.name = "mtcrate"
crate.description = "test crate"
res = add_schema_to_crate(schema, crate)
print(json.dumps(res))
if __name__ == "__main__":
main()
# # Define properties for Creator
# has_name = TypeProperty(
# id="hasName",
#
# range_includes_data_type=[LiteralType.STRING]
# )
# has_identifier = TypeProperty(
# id="hasIdentifier",
# range_includes_data_type=[LiteralType.STRING],
# ontological_annotations=["https://www.dublincore.org/specifications/dublin-core/dcmi-terms/elements11/identifier/"]
# )
# # Define the Creator type with its properties
# creator_type = Type(
# id="Creator",
# type="Type",
# subclass_of=["https://schema.org/Thing"],
# ontological_annotations=["https://www.dublincore.org/specifications/dublin-core/dcmi-terms/terms/creator//"],
# rdfs_property=[has_name, has_identifier],
# comment="",
# label="",
# restrictions=None
# )
# # Define properties for TextResource
# has_date_submitted = TypeProperty(
# id="hasDateSubmitted",
# range_includes_data_type=[LiteralType.DATETIME],
# ontological_annotations=["https://www.dublincore.org/specifications/dublin-core/dcmi-terms/terms/dateSubmitted/"]
# )
# has_creator = TypeProperty(
# id="hasCreator",
# range_includes=[creator_type.id]
# )
# # Define the TextResource type with its properties
# text_resource_type = Type(
# id="TextResource",
# type="Type",
# subclass_of=["https://schema.org/Thing"],
# ontological_annotations=["https://www.dublincore.org/specifications/dublin-core/dcmi-terms/dcmitype/Text/"],
# rdfs_property=[has_date_submitted, has_creator],
# comment="",
# label="",
# restrictions=None
# )
# # Create metadata entries
# creator_entry = MetadataEntry(
# id="creator1",
# types={creator_type.id},
# props={"hasName": "John Author", "hasIdentifier": "https://orcid.org/0000-0000-0000-0000"},
# references={}
# )
# text_resource_entry = MetadataEntry(
# id="TextResource1",
# types={text_resource_type.id},
# props={"hasDate": "2025-01-21T07:12:20Z"},
# references={"hasCreator": ["creator1"]}
# )
# print(creator_entry.model_dump_json())