Create Signposting from RO-Crate (#2)

* improvate jsonld parsing to be compatible with ro-crate

* refactoring

* example with RO-Crate

* bump version

---------

Co-authored-by: Daniel Bauer <daniel.bauer@senckenberg.de>
This commit is contained in:
Daniel Bauer
2024-11-21 15:40:46 +01:00
committed by GitHub
parent f97c33e418
commit 6b1ea1403c
8 changed files with 216 additions and 84 deletions

View File

@@ -1,14 +1,12 @@
from typing import Callable from typing import Callable
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from signposting import Signpost, LinkRel from signposting import Signpost
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re
import json import json
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.conf import settings from django.conf import settings
from rdflib import Graph from .utils import jsonld_to_signposts
from . import sparql
class SignpostingMiddleware: class SignpostingMiddleware:
@@ -45,68 +43,6 @@ class SignpostingMiddleware:
class JsonLdSignpostingParserMiddleware(MiddlewareMixin): class JsonLdSignpostingParserMiddleware(MiddlewareMixin):
def is_url(self, url: str) -> bool:
url_pattern = re.compile(
r"^(https?|ftp)://" # protocol
r"(?:(?:[a-zA-Z0-9-_]+\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,6})" # domain
r"(?::\d{1,5})?" # optional port
r"(?:/.*)?$" # path
)
return bool(url_pattern.match(url))
def select_url(self, elements: tuple[str, ...]) -> str | None:
for elem in elements[::-1]:
if self.is_url(str(elem)):
return str(elem)
return None
def _jsonld_to_signposts(self, jsonld: dict) -> dict:
signposts = []
# TODO use jsonld context in query as prefix
g = Graph().parse(data=json.dumps(jsonld), format="json-ld")
rootElement = next(iter(sparql.root_element_query(g)), None)
if rootElement:
rootElement = rootElement[0]
else:
print("No root element found")
return {}
types = sparql.type_query(g, rootElement)
for type in types:
signposts.append(Signpost(LinkRel.type, str(type[0])))
authors = sparql.author_query(g, rootElement)
for author in authors:
author = self.select_url(author)
if author:
signposts.append(Signpost(LinkRel.author, author))
license = next(iter(sparql.license_query(g, rootElement)), [])
license = self.select_url(license)
if license:
signposts.append(Signpost(LinkRel.license, license))
citations = sparql.cite_query(g, rootElement)
for citation in citations:
citation = self.select_url(citation)
if citation:
signposts.append(Signpost(LinkRel.cite_as, citation))
sameas = sparql.sameas_query(g, rootElement)
for sa in sameas:
sa_media_type = sa[-1]
sa = self.select_url(sa[:-1])
if sa:
signposts.append(Signpost(LinkRel.describedby, sa, sa_media_type))
items = sparql.item_query(g, rootElement)
for item in items:
item_media_type = item[-1]
item = self.select_url(item[:-1])
if item:
signposts.append(Signpost(LinkRel.item, item, item_media_type))
return signposts
def process_response( def process_response(
self, request: HttpRequest, response: HttpResponse self, request: HttpRequest, response: HttpResponse
@@ -119,7 +55,7 @@ class JsonLdSignpostingParserMiddleware(MiddlewareMixin):
for script in soup.find_all("script", type="application/ld+json"): for script in soup.find_all("script", type="application/ld+json"):
try: try:
jsonld = json.loads(script.string) jsonld = json.loads(script.string)
signposts = self._jsonld_to_signposts(jsonld) signposts = jsonld_to_signposts(jsonld)
response._signposts = signposts response._signposts = signposts
except json.JSONDecodeError as e: except json.JSONDecodeError as e:

View File

@@ -14,7 +14,21 @@ WHERE {
(schema:license | schema:author | schema:creator ) ?value . (schema:license | schema:author | schema:creator ) ?value .
# No incoming edges for ?rootElement # No incoming edges for ?rootElement
FILTER NOT EXISTS { ?s ?p ?rootElement } FILTER NOT EXISTS {
?incoming ?p ?rootElement .
# also allow incoming edges that link entities BACK to the dataset
FILTER(?p != schema:isPartOf)
FILTER(?p != schema:mainEntityOfPage)
FILTER(?p != schema:recordedIn)
FILTER(?p != schema:exampleOfWork)
FILTER(?p != schema:includedInDataCatalogue)
FILTER(?p != schema:subjectOf)
FILTER(?p != schema:dataset)
# allow incoming edges from ro-crate-metadata.json
FILTER(CONTAINS(?incoming, "ro-crate-metadata.json")) .
}
} }
LIMIT 1 LIMIT 1
""") """)

View File

@@ -1,5 +1,10 @@
import json
from django.http import HttpResponse from django.http import HttpResponse
from signposting import Signpost from rdflib import Graph
from signposting import Signpost, LinkRel
import re
from django_signposting import sparql
def add_signposts(response: HttpResponse, *args: Signpost): def add_signposts(response: HttpResponse, *args: Signpost):
@@ -15,3 +20,67 @@ def add_signposts(response: HttpResponse, *args: Signpost):
for signpost in args: for signpost in args:
if signpost not in response._signposts: if signpost not in response._signposts:
response._signposts.append(signpost) response._signposts.append(signpost)
def select_url(elements: tuple[str, ...]) -> str | None:
def is_url(url: str) -> bool:
url_pattern = re.compile(
r"^(https?|ftp)://" # protocol
r"(?:(?:[a-zA-Z0-9-_]+\.)?[a-zA-Z0-9-]+\.[a-zA-Z]{2,6})" # domain
r"(?::\d{1,5})?" # optional port
r"(?:/.*)?$" # path
)
return bool(url_pattern.match(url))
for elem in elements[::-1]:
if is_url(str(elem)):
return str(elem)
return None
def jsonld_to_signposts(jsonld: dict) -> []:
signposts = []
# TODO use jsonld context in query as prefix
g = Graph().parse(data=json.dumps(jsonld), format="json-ld")
rootElement = next(iter(sparql.root_element_query(g)), None)
if rootElement:
rootElement = rootElement[0]
else:
print("No root element found")
return {}
types = sparql.type_query(g, rootElement)
for type in types:
signposts.append(Signpost(LinkRel.type, str(type[0])))
authors = sparql.author_query(g, rootElement)
for author in authors:
author = select_url(author)
if author:
signposts.append(Signpost(LinkRel.author, author))
license = next(iter(sparql.license_query(g, rootElement)), [])
license = select_url(license)
if license:
signposts.append(Signpost(LinkRel.license, license))
citations = sparql.cite_query(g, rootElement)
for citation in citations:
citation = select_url(citation)
if citation:
signposts.append(Signpost(LinkRel.cite_as, citation))
sameas = sparql.sameas_query(g, rootElement)
for sa in sameas:
sa_media_type = sa[-1]
sa = select_url(sa[:-1])
if sa:
signposts.append(Signpost(LinkRel.describedby, sa, sa_media_type))
items = sparql.item_query(g, rootElement)
for item in items:
item_media_type = item[-1]
item = select_url(item[:-1])
if item:
signposts.append(Signpost(LinkRel.item, item, item_media_type))
return signposts

View File

@@ -21,4 +21,5 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.SimpleView.as_view()), path("", views.SimpleView.as_view()),
path("jsonld", views.JsonLdView.as_view()), path("jsonld", views.JsonLdView.as_view()),
path("ro-crate", views.rocrate.as_view()),
] ]

View File

@@ -1,15 +1,13 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.views import View
from django.shortcuts import render from django.shortcuts import render
from django.views import View
from django_json_ld.views import JsonLdContextMixin from django_json_ld.views import JsonLdContextMixin
from django_signposting.utils import add_signposts from signposting import LinkRel, Signpost
from signposting import Signpost, LinkRel from django_signposting.utils import add_signposts, jsonld_to_signposts
class SimpleView(View): class SimpleView(View):
def get(self, request): def get(self, request):
response = HttpResponse("Hello, world!") response = HttpResponse("Hello, world!")
@@ -17,14 +15,13 @@ class SimpleView(View):
add_signposts( add_signposts(
response, response,
Signpost(LinkRel.type, "http://schema.org/Dataset"), Signpost(LinkRel.type, "http://schema.org/Dataset"),
Signpost(LinkRel.author, "https://orcid.org/0000-0001-9447-460X") Signpost(LinkRel.author, "https://orcid.org/0000-0001-9447-460X"),
) )
return response return response
class JsonLdView(JsonLdContextMixin, View): class JsonLdView(JsonLdContextMixin, View):
sd = { sd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": ["WebSite", "Dataset"], "@type": ["WebSite", "Dataset"],
@@ -35,12 +32,12 @@ class JsonLdView(JsonLdContextMixin, View):
{ {
"@type": "MediaObject", "@type": "MediaObject",
"contentUrl": "https://example.com/download.zip", "contentUrl": "https://example.com/download.zip",
"encodingFormat": "application/zip" "encodingFormat": "application/zip",
}, },
{ {
"@type": "MediaObject", "@type": "MediaObject",
"contentUrl": "https://example.com/metadata.json", "contentUrl": "https://example.com/metadata.json",
} },
], ],
"author": { "author": {
"@type": "Person", "@type": "Person",
@@ -50,21 +47,85 @@ class JsonLdView(JsonLdContextMixin, View):
"license": { "license": {
"@type": "CreativeWork", "@type": "CreativeWork",
"name": "CC BY 4.0", "name": "CC BY 4.0",
"url": "https://creativecommons.org/licenses/by/4.0/" "url": "https://creativecommons.org/licenses/by/4.0/",
}, },
"hasPart": [ "hasPart": [
{ {
"@type": "ImageObject", "@type": "ImageObject",
"url": "http://example.com/image.png", "url": "http://example.com/image.png",
"encodingFormat": "image/png" "encodingFormat": "image/png",
}, },
{ {
"@type": "ImageObject", "@type": "ImageObject",
"url": "http://example.com/image2.png", "url": "http://example.com/image2.png",
"encodingFormat": "image/png" "encodingFormat": "image/png",
} },
] ],
} }
def get(self, request): def get(self, request):
return render(request, "jsonld.html", context={"sd": self.sd}) return render(request, "jsonld.html", context={"sd": self.sd})
class rocrate(JsonLdContextMixin, View):
def get(self, request):
import json
import os
import tempfile
# Build an RO-Crate and serve its preview
from rocrate.model.person import Person
from rocrate.model.creativework import CreativeWork
from rocrate.rocrate import ROCrate
with tempfile.TemporaryDirectory() as d:
crate = ROCrate(gen_preview=True)
crate.add_file(
"http://example.com/test.pdf",
properties={"name": "test file", "encodingFormat": "application/pdf"},
)
author = crate.add(
Person(
crate,
"https://orcid.org/0000-0001-9447-460X",
properties={"name": "Daniel Bauer"},
)
)
license = crate.add(
CreativeWork(
crate,
"https://spdx.org/licenses/CC0-1.0",
properties={
"@id": "https://spdx.org/licenses/CC0-1.0",
"@type": "CreativeWork",
"name": "CC0-1.0",
"description": "Creative Commons Zero v1.0 Universal",
},
)
)
sameAs = crate.add(
CreativeWork(
crate,
"https://example.com/ro-crate-metadata.json",
properties={"encodingFormat": "application/ld+json"},
)
)
crate.root_dataset["name"] = "My Dataset"
crate.root_dataset["description"] = "A dataset of things."
crate.root_dataset["author"] = author
crate.root_dataset["creator"] = author
crate.root_dataset["license"] = license
crate.root_dataset["url"] = "https://example.com"
crate.root_dataset["sameAs"] = sameAs
crate.write(d)
# Extract JSON-LD from RO-Crate metadata and build signposts from it
metadata = open(os.path.join(d, "ro-crate-metadata.json"), "r").read()
metadata = json.loads(metadata)
signposts = jsonld_to_signposts(metadata)
preview = open(os.path.join(d, "ro-crate-preview.html"), "r").read()
response = HttpResponse(preview)
add_signposts(response, *signposts)
return response

View File

@@ -15,3 +15,4 @@ signposting==0.9.9
soupsieve==2.6 soupsieve==2.6
sqlparse==0.5.1 sqlparse==0.5.1
urllib3==2.2.3 urllib3==2.2.3
rocrate==0.11.0

View File

@@ -8,7 +8,7 @@ authors = [
{name = "Daniel Bauer", email = "github@dbauer.me"} {name = "Daniel Bauer", email = "github@dbauer.me"}
] ]
description = "Add FAIR signpostings to response headers in Django" description = "Add FAIR signpostings to response headers in Django"
version = "0.10.1" version = "0.10.2"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@@ -218,3 +218,53 @@ def test_jsonld_signposting_multiple_types():
Signpost(LinkRel.author, "http://example.com/author"), Signpost(LinkRel.author, "http://example.com/author"),
], ],
) )
def test_jsonld_signposting_rocrate():
jsonld_test_runner(
{
"@context": "http://w3id.org/ro/crate/1.1/context",
"@graph": [
{
"@type": "CreativeWork",
"@id": "ro-crate-metadata.json",
"conformsTo": {"@id": "http://w3id.org/ro/crate/1.1"},
"about": {"@id": "http://example.com/myCrate"}
},
{
"@type": "Dataset",
"@id": "http://example.com/myCrate",
"author": {"@id": "http://example.com/author"},
"datePublished": "2024",
"name": "Test crate",
"description": "This is a detached test create",
"license": {"@id": "http://example.com/license"},
"hasPart": [
{"@id": "http://example.com/file"}
]
},
{
"@id": "http://example.com/author",
"@type": "Person",
"name": "Daniel Bauer"
},
{
"@id": "http://example.com/license",
"@type": "CreativeWork",
"name": "Test License"
},
{
"@id": "http://example.com/file",
"@type": "File",
"contentUrl": "http://example.com/image1.jpg",
"encodingFormat": "image/jpeg"
}
]
},
[
Signpost(LinkRel.type, "http://schema.org/Dataset"),
Signpost(LinkRel.author, "http://example.com/author"),
Signpost(LinkRel.license, "http://example.com/license"),
Signpost(LinkRel.item, "http://example.com/image1.jpg", "image/jpeg"),
],
)