mirror of
https://github.com/researchobjectschema/ro-crate-interoperability-profile.git
synced 2026-08-07 06:12:26 +02:00
Successfully emitting valid (!) ro-crate
This commit is contained in:
+38
-16
@@ -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))
|
||||
|
||||
+3
-3
@@ -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))
|
||||
|
||||
+43
-1
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user