Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(generic): refactor GenericRDFBaseRenderer and add CIDOC renderers #1505

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion apis_core/apis_entities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.urls import NoReverseMatch, reverse

from apis_core.apis_metainfo.models import RootObject, Uri
from apis_core.utils.settings import apis_base_uri

NEXT_PREV = getattr(settings, "APIS_NEXT_PREV", True)

Expand Down Expand Up @@ -94,7 +95,7 @@ def create_default_uri(sender, instance, created, raw, using, update_fields, **k
skip_default_uri = getattr(instance, "skip_default_uri", False)
if create_default_uri and not skip_default_uri:
if isinstance(instance, AbstractEntity) and created:
base = getattr(settings, "APIS_BASE_URI", "https://example.org").strip("/")
base = apis_base_uri().strip("/")
try:
route = reverse("GetEntityGenericRoot", kwargs={"pk": instance.pk})
except NoReverseMatch:
Expand Down
173 changes: 172 additions & 1 deletion apis_core/apis_entities/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from rdflib import Literal, Namespace, URIRef
from rdflib.namespace import GEO, RDF, RDFS, XSD
from rest_framework import serializers

from apis_core.generic.serializers import GenericHyperlinkedIdentityField
from apis_core.generic.serializers import (
GenericHyperlinkedIdentityField,
GenericModelCidocSerializer,
)


class MinimalEntitySerializer(serializers.Serializer):
Expand All @@ -11,3 +16,169 @@ class MinimalEntitySerializer(serializers.Serializer):

def get_name(self, object) -> str:
return str(object)


class E53_PlaceCidocSerializer(GenericModelCidocSerializer):
def to_representation(self, instance):
g = super().to_representation(instance)

crm_namespace = Namespace("http://www.cidoc-crm.org/cidoc-crm/")
attributes_namespace = Namespace(f"{self.base_uri}/attributes/")
g.add((self.instance_uri, RDF.type, crm_namespace.E53_Place))
if instance.latitude is not None and instance.longitude is not None:
node_spaceprimitive = attributes_namespace[f"spaceprimitive.{instance.id}"]
g.add(
(
self.instance_uri,
crm_namespace.P168_place_is_defined_by,
node_spaceprimitive,
)
)
g.add(
(
node_spaceprimitive,
RDF.type,
crm_namespace.E94_Space_Primitive,
)
)
g.add(
(
node_spaceprimitive,
crm_namespace.P168_place_is_defined_by,
Literal(
(
f"Point ( {'+' if instance.longitude > 0 else ''}{instance.longitude} {'+' if instance.latitude > 0 else ''}{instance.latitude} )"
),
datatype=GEO.wktLiteral,
),
)
)
return g


class E21_PersonCidocSerializer(GenericModelCidocSerializer):
def to_representation(self, instance):
g = super().to_representation(instance)

crm_namespace = Namespace("http://www.cidoc-crm.org/cidoc-crm/")
appellation_namespace = Namespace(f"{self.base_uri}/appellation/")
attributes_namespace = Namespace(f"{self.base_uri}/attributes/")
g.add((self.instance_uri, RDF.type, crm_namespace.E21_Person))

if hasattr(instance, "forename"):
forename_uri = URIRef(appellation_namespace[f"forename_{instance.id}"])
g.add(
(
forename_uri,
RDF.type,
crm_namespace.E33_E41_Linguistic_Appellation,
)
)
g.add(
(
self.appellation_uri,
crm_namespace.P106_is_composed_of,
forename_uri,
)
)
g.add((forename_uri, RDFS.label, Literal(instance.forename)))

if hasattr(instance, "surname"):
surname_uri = URIRef(appellation_namespace[f"surname_{instance.id}"])
g.add(
(
surname_uri,
RDF.type,
crm_namespace.E33_E41_Linguistic_Appellation,
)
)
g.add(
(
self.appellation_uri,
crm_namespace.P106_is_composed_of,
surname_uri,
)
)
g.add((surname_uri, RDFS.label, Literal(instance.surname)))

if instance.start_date_written:
birth_event = URIRef(attributes_namespace[f"birth_{instance.id}"])
birth_time_span = URIRef(
attributes_namespace[f"birth_time-span_{instance.id}"]
)
g.add((birth_event, RDF.type, crm_namespace.E67_Birth))
g.add(
(
birth_event,
crm_namespace.P98_brought_into_life,
self.instance_uri,
)
)
g.add(
(
birth_event,
crm_namespace["P4_has_time-span"],
birth_time_span,
)
)
g.add((birth_time_span, RDF.type, crm_namespace["E52_Time-Span"]))
g.add(
(
birth_time_span,
crm_namespace.P82a_begin_of_the_begin,
Literal(instance.start_date, datatype=XSD.date)
if instance.start_date is not None
else Literal(instance.start_date_written),
)
)
g.add(
(
birth_time_span,
crm_namespace.P82b_end_of_the_end,
Literal(instance.start_date, datatype=XSD.date)
if instance.start_date is not None
else Literal(instance.start_date_written),
)
)

if instance.end_date_written:
death_event = URIRef(attributes_namespace[f"death_{instance.id}"])
death_time_span = URIRef(
attributes_namespace[f"death_time-span_{instance.id}"]
)
g.add((death_event, RDF.type, crm_namespace.E69_Death))
g.add(
(
death_event,
crm_namespace.P100_was_death_of,
self.instance_uri,
)
)
g.add(
(
death_event,
crm_namespace["P4_has_time-span"],
death_time_span,
)
)
g.add((death_time_span, RDF.type, crm_namespace["E52_Time-Span"]))
g.add(
(
death_time_span,
crm_namespace.P82a_begin_of_the_begin,
Literal(instance.end_date, datatype=XSD.date)
if instance.end_date is not None
else Literal(instance.end_date_written),
)
)
g.add(
(
death_time_span,
crm_namespace.P82b_end_of_the_end,
Literal(instance.end_date, datatype=XSD.date)
if instance.end_date is not None
else Literal(instance.end_date_written),
)
)

return g
77 changes: 63 additions & 14 deletions apis_core/generic/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,81 @@

from rdflib import Graph
from rest_framework import renderers
from rest_framework.exceptions import APIException

logger = logging.getLogger(__name__)


class GenericRDFBaseRenderer(renderers.BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
g = Graph()
for result in data.get("results", []):
match result:
case tuple(_, _, _):
g.add(result)
case other:
logger.debug("Could not add %s to RDF graph: not a tuple", other)
return g.serialize(format=self.media_type)
"""
Base class to render RDF graphs to various formats.
This renderer expects the serialized data to either be a rdflib grap **or**
to contain a list of rdflib graphs. If it works with a list of graphs, those
are combined to one graph.
This graph is then serialized and the result is returned. The serialization
format can be set using the `rdflib_format` attribute. If this is not set, the
`format` attribute of the renderer is used as serialization format (this is the
format as it is used by the Django Rest Framework for content negotiation.
"""

format = "ttl"
rdflib_format = None

class GenericRDFXMLRenderer(GenericRDFBaseRenderer):
media_type = "application/rdf+xml"
format = "rdf+xml"
def render(self, data, accepted_media_type=None, renderer_context=None):
result = Graph()

match data:
case {"results": results, **rest}: # noqa: F841
# Handle case where data is a dict with multiple graphs
for graph in results:
if isinstance(graph, Graph):
# Merge triples
for triple in graph:
result.add(triple)
# Merge namespace bindings
for prefix, namespace in graph.namespaces():
result.bind(prefix, namespace, override=False)
case {"detail": detail}:
raise APIException(detail)
case Graph():
# Handle case where data is a single graph
result = data
# Ensure namespaces are properly bound in the single graph case
for prefix, namespace in data.namespaces():
result.bind(prefix, namespace, override=False)
case _:
raise ValueError(
"Invalid data format. Expected rdflib Graph or dict with 'results' key containing graphs"
)
serialization_format = self.rdflib_format or self.format
return result.serialize(format=serialization_format)


class GenericRDFTurtleRenderer(GenericRDFBaseRenderer):
format = "ttl"
media_type = "text/turtle"
format = "rdf+turtle"
rdflib_format = "turtle"


class GenericRDFXMLRenderer(GenericRDFBaseRenderer):
format = "rdf"
media_type = "application/rdf+xml"
rdflib_format = "xml"


class GenericRDFN3Renderer(GenericRDFBaseRenderer):
format = "rdf"
media_type = "text/n3"
format = "rdf+n3"
rdflib_format = "n3"


class CidocTTLRenderer(GenericRDFBaseRenderer):
format = "cidoc"
media_type = "text/ttl"
rdflib_format = "ttl"


class CidocXMLRenderer(GenericRDFBaseRenderer):
format = "cidoc"
media_type = "application/rdf+xml"
rdflib_format = "xml"
63 changes: 63 additions & 0 deletions apis_core/generic/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from rdflib import Graph, Literal, Namespace, URIRef
from rdflib.namespace import GEO, OWL, RDF, RDFS
from rest_framework.reverse import reverse
from rest_framework.serializers import (
BaseSerializer,
CharField,
HyperlinkedModelSerializer,
HyperlinkedRelatedField,
Expand All @@ -10,6 +13,8 @@
SerializerMethodField,
)

from apis_core.utils.settings import apis_base_uri, rdf_namespace_prefix


class GenericHyperlinkedRelatedField(HyperlinkedRelatedField):
def get_url(self, obj, view_name, request, format):
Expand Down Expand Up @@ -87,3 +92,61 @@ def get_content_type_key(self, obj) -> str:
def get_content_type_name(self, obj) -> str:
content_type = ContentType.objects.get_for_model(obj)
return content_type.name


class GenericModelCidocSerializer(BaseSerializer):
def __init__(self, *args, **kwargs):
self.base_uri = f"{apis_base_uri()}"
self.rdf_nsp_base = rdf_namespace_prefix()
self.appellation_nsp_prefix = f"{self.rdf_nsp_base}-appellation"
self.attr_nsp_prefix = f"{self.rdf_nsp_base}-attr"
super().__init__(*args, **kwargs)

def to_representation(self, instance):
g = Graph()
content_type = ContentType.objects.get_for_model(instance)

crm_namespace = Namespace("http://www.cidoc-crm.org/cidoc-crm/")
g.namespace_manager.bind("crm", crm_namespace, replace=True)
g.namespace_manager.bind("owl", OWL, replace=True)
g.namespace_manager.bind("geo", GEO, replace=True)

appellation_namespace = Namespace(f"{self.base_uri}/appellation/")
g.namespace_manager.bind(
self.appellation_nsp_prefix, appellation_namespace, replace=True
)
attributes_namespace = Namespace(f"{self.base_uri}/attributes/")
g.namespace_manager.bind(
self.attr_nsp_prefix, attributes_namespace, replace=True
)

self.instance_nsp_prefix = f"{self.rdf_nsp_base}-{content_type.name.lower()}"
instance_namespace = Namespace(self.base_uri + instance.get_listview_url())
g.namespace_manager.bind(self.instance_nsp_prefix, instance_namespace)

self.instance_uri = URIRef(instance_namespace[str(instance.id)])

# Add sameAs links
for uri in instance.uri_set.all():
uri_ref = URIRef(uri.uri)
g.add((self.instance_uri, OWL.sameAs, uri_ref))

# Add properties
self.appellation_uri = URIRef(appellation_namespace[str(instance.id)])
g.add(
(
self.appellation_uri,
RDF.type,
crm_namespace.E33_E41_Linguistic_Appellation,
)
)
g.add(
(
self.instance_uri,
crm_namespace.P1_is_identified_by,
self.appellation_uri,
)
)
g.add((self.appellation_uri, RDFS.label, Literal(str(instance))))

return g
Loading
Loading