diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/metadata_entry.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/metadata_entry.py index e800903..d0dc799 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/metadata_entry.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/metadata_entry.py @@ -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)) diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/owl_restriction.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/owl_restriction.py index 51d3288..a2348c9 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/owl_restriction.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/owl_restriction.py @@ -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)) diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/rdfs_class.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/rdfs_class.py index a0563cd..f4b6a18 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/rdfs_class.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/rdfs_class.py @@ -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: diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/restriction.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/restriction.py index 065d5dc..011127c 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/restriction.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/restriction.py @@ -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)) diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_facade.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_facade.py index 3d3beac..6e94e2d 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_facade.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/schema_facade.py @@ -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() \ No newline at end of file diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type.py index 3289253..58546b3 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type.py @@ -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), diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type_property.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type_property.py index 90cd97b..c66c37b 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type_property.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/crate/type_property.py @@ -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)) diff --git a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/example/examples.py b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/example/examples.py index 7bce383..03e1fdd 100644 --- a/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/example/examples.py +++ b/0.2.x/lib/python/lib-ro-crate-schema/src/lib_ro_crate_schema/example/examples.py @@ -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())