diff --git a/ckanext/dcat/codelists/__init__.py b/ckanext/dcat/codelists/__init__.py
new file mode 100644
index 00000000..43737cd4
--- /dev/null
+++ b/ckanext/dcat/codelists/__init__.py
@@ -0,0 +1,8 @@
+
+from .extract import extract
+from pathlib import Path
+
+high_value_dataset_category = extract(Path(__file__).parent / 'high-value-dataset-category.rdf')
+
+
+__all__ = ['high_value_dataset_category']
diff --git a/ckanext/dcat/codelists/extract.py b/ckanext/dcat/codelists/extract.py
new file mode 100644
index 00000000..521c3e22
--- /dev/null
+++ b/ckanext/dcat/codelists/extract.py
@@ -0,0 +1,116 @@
+'''
+Converts EU RDF descriptions of codelists to ckanext-scheming choices.
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Meteorological
+ Meteorología
+ Meteoroloģijas datu kopas
+ Data meteoroloġika
+ Meteorologische data
+ Meteorologiska data
+ ...
+ 3
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 3
+
+
+
+
+to:
+
+ {
+ "label": {
+ "en": "Meteorological",
+ "ga": "Meit\u00e9areola\u00edoch",
+ "mt": "Data meteorolo\u0121ika"
+ },
+ "value": "http://data.europa.eu/bna/c_164e0bf5"
+ },
+
+
+These are suitable for inclusion in the schema.json.
+
+'''
+
+import rdflib
+import rdflib.parser
+
+from rdflib.namespace import Namespace, RDF, SKOS
+
+from pathlib import Path
+import xml
+import json
+from functools import lru_cache
+
+from ckan.plugins import toolkit
+
+import logging
+log = logging.getLogger(__name__)
+
+
+EUVOC = Namespace("http://publications.europa.eu/ontology/euvoc#")
+# filter out variants of languages, en_GB doesn't match en.
+LANGS = set(l.split('_')[0] for l in toolkit.aslist(toolkit.config.get('ckan.locales_offered', ['en'])))
+
+
+class Codelist:
+ def __init__(self, choices, scheme):
+ self.choices = choices
+ self.scheme = scheme
+ self.choices_map = {elt['value']:elt['label'] for elt in choices}
+ log.debug(LANGS)
+ def labels(self, val):
+ return self.choices_map.get(val, {})
+
+@lru_cache(None)
+def extract(f:Path):
+
+ g = rdflib.ConjunctiveGraph()
+ try:
+ g.parse(data=f.read_text(), format='xml')
+ # Apparently there is no single way of catching exceptions from all
+ # rdflib parsers at once, so if you use a new one and the parsing
+ # exceptions are not cached, add them here.
+ # PluginException indicates that an unknown format was passed.
+ except (SyntaxError, xml.sax.SAXParseException,
+ rdflib.plugin.PluginException, TypeError) as e:
+ raise Exception(e)
+
+ choices = {}
+
+ for subject in g.subjects(RDF.type, SKOS.Concept):
+ labels = {l.language: str(l) for l in g.objects(subject, SKOS.prefLabel) if l.language in LANGS }
+ try:
+ order = list(g.objects(subject, EUVOC.order))[0]
+ except KeyError:
+ order = str(subject)
+
+ choice = {"label": labels,
+ "value": str(subject)}
+
+ choices[order] = choice
+
+ ordered_choices = [v for k,v in sorted(choices.items())]
+
+ scheme = None
+ for subject in g.subjects(RDF.type, SKOS.ConceptScheme):
+ scheme = str(subject)
+
+ return Codelist(ordered_choices, scheme)
+
+
+def write_json():
+ for path in Path(__file__).parent.glob('*.rdf'):
+ data = extract(path)
+ dest = path.parent / (path.stem + '.json')
+ with open (dest, 'w') as f:
+ json.dump(data.choices, f, indent=2)
+
+
+#print(json.dumps(ordered_choices, indent=2))
+
+
+
diff --git a/ckanext/dcat/codelists/high-value-dataset-category.rdf b/ckanext/dcat/codelists/high-value-dataset-category.rdf
new file mode 100644
index 00000000..93e0f7cc
--- /dev/null
+++ b/ckanext/dcat/codelists/high-value-dataset-category.rdf
@@ -0,0 +1,226 @@
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-27
+ 2023-09-07
+ High-value dataset categories
+
+ High-value dataset categories
+ 1.0
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Метеорологични данни
+ Meteorologie
+ Meteorologiske data
+ Meteorologie
+ Μετεωρολογικές πληροφορίες
+ Meteorological
+ Meteorología
+ Meteoroloogiateave
+ Säätiedot
+ Météorologiques
+ Meitéareolaíoch
+ Meteorološki podatci
+ Meteorológiai adatok
+ Dati meteorologici
+ Meteorologiniai duomenys
+ Meteoroloģijas datu kopas
+ Data meteoroloġika
+ Meteorologische data
+ Dane meteorologiczne
+ Meteorológicas
+ Domeniul meteorologic
+ Meteorológia
+ Meteorološki podatki
+ Meteorologiska data
+ 3
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 3
+
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Дружества и собственост на дружествата
+ Společnosti a vlastnictví společností
+ Virksomheder og virksomhedsejerskab
+ Unternehmen und Eigentümerschaft von Unternehmen
+ Εταιρείες και ιδιοκτησιακό καθεστώς εταιρειών
+ Companies and company ownership
+ Sociedades y propiedad de sociedades
+ Äriühingud ja äriühingu omandisuhted
+ Yritys- ja yritysten omistustiedot
+ Entreprises et propriété d'entreprises
+ Cuideachtaí agus úinéireacht cuideachtaí
+ Trgovačka društva i vlasništvo nad trgovačkim društvima
+ Vállalati és vállalattulajdonosi adatok
+ Dati relativi alle imprese e alla proprietà delle imprese
+ Bendrovės ir bendrovių valdymas nuosavybės teise
+ Uzņēmumi un uzņēmumu īpašumtiesības
+ Data dwar il-kumpanniji u l-proprjetà tal-kumpanniji
+ Bedrijven en eigendom van bedrijven
+ Dane dotyczące przedsiębiorstw i ich własności
+ Empresas e propriedade de empresas
+ Domeniul Societăți și structura de proprietate a societăților
+ Spoločnosti a vlastníctvo spoločností
+ Družbe in lastništvo družb
+ Företag och företagsägande
+ 5
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 5
+
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Геопространствени данни
+ Geoprostorové údaje
+ Geospatiale data
+ Georaum
+ Γεωχωρικές πληροφορίες
+ Geospatial
+ Geoespacial
+ Georuumilised andmed
+ Paikkatiedot
+ Géospatiales
+ Geospásúil
+ Geoprostorni podatci
+ Térinformatikai adatok
+ Dati geospaziali
+ Geoerdviniai duomenys
+ Ģeotelpisko datu kopas
+ Data ġeospazjali
+ Geospatiale data
+ Dane geoprzestrzenne
+ Geoespaciais
+ Domeniul geospațial
+ Geopriestorové údaje
+ Geoprostorski podatki
+ Geospatiala data
+ 1
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 1
+
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Мобилност
+ Mobilita
+ Mobilitet
+ Mobilität
+ Κινητικότητα
+ Mobility
+ Movilidad
+ Liikuvus
+ Liikkuvuustiedot
+ Mobilité
+ Soghluaisteacht
+ Mobilnost
+ Mobilitási adatok
+ Dati relativi alla mobilità
+ Judumas
+ Mobilitāte
+ Data dwar il-mobbiltà
+ Mobiliteit
+ Dane dotyczące mobilności
+ Mobilidade
+ Domeniul Mobilitate
+ Mobilita
+ Mobilnost
+ Rörlighet
+ 6
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 6
+
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Наблюдение на Земята и околната среда
+ Země a životní prostředí
+ Jordobservation og miljø
+ Erdbeobachtung und Umwelt
+ Γεωσκόπηση και περιβάλλον
+ Earth observation and environment
+ Observación de la Tierra y medio ambiente
+ Maa seire ja keskkond
+ Maan havainnointi ja ympäristö
+ Observation de la terre et environnement
+ Faire na cruinne agus an comhshaol
+ Promatranje Zemlje i okoliš
+ Földmegfigyelési és környezeti adatok
+ Dati relativi all'osservazione della terra e all'ambiente
+ Žemės stebėjimas ir aplinka
+ Zemes novērošana un vide
+ Data dwar l-osservazzjoni tad-dinja u l-ambjent
+ Aardobservatie en milieu
+ Dane dotyczące obserwacji Ziemi i środowiska
+ Observação da Terra e do ambiente
+ Domeniul Observarea Pământului și mediu
+ Pozorovanie Zeme a životné prostredie
+ Opazovanje zemlje in okolje
+ Jordobservation och miljö
+ 2
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 2
+
+
+
+
+
+ 2019-07-06
+ 2023-09-05
+ 2023-09-13
+
+ Статистика
+ Statistika
+ Statistik
+ Statistik
+ Στατιστικές
+ Statistics
+ Estadística
+ Statistika
+ Tilastotiedot
+ Statistiques
+ Staidreamh
+ Statistički podatci
+ Statisztikák
+ Dati statistici
+ Statistika
+ Statistika
+ Data statistika
+ Statistiek
+ Dane statystyczne
+ Estatísticas
+ Domeniul statistic
+ Štatistika
+ Statistični podatki
+ Statistik
+ 4
+ data sets as described in Commission Implementing Regulation (EU) 2023/138 of 21 December 2022 laying down a list of specific high-value datasets and the arrangements for their publication and re-use, Annex, Section 4
+
+
+
+
+
\ No newline at end of file
diff --git a/ckanext/dcat/profiles.py b/ckanext/dcat/profiles.py
index 450166b0..acc6896a 100644
--- a/ckanext/dcat/profiles.py
+++ b/ckanext/dcat/profiles.py
@@ -21,6 +21,7 @@
from ckan.plugins import toolkit
from ckan.lib.munge import munge_tag
from ckanext.dcat.utils import resource_uri, publisher_uri_organization_fallback, DCAT_EXPOSE_SUBCATALOGS, DCAT_CLEAN_TAGS
+from . import codelists
DCT = Namespace("http://purl.org/dc/terms/")
DCAT = Namespace("http://www.w3.org/ns/dcat#")
@@ -34,6 +35,7 @@
GSP = Namespace('http://www.opengis.net/ont/geosparql#')
OWL = Namespace('http://www.w3.org/2002/07/owl#')
SPDX = Namespace('http://spdx.org/rdf/terms#')
+ELI= Namespace('http://data.europa.eu/eli/ontology#')
GEOJSON_IMT = 'https://www.iana.org/assignments/media-types/application/vnd.geo+json'
@@ -727,12 +729,17 @@ def _add_triples_from_dict(self, _dict, subject, items,
list_value=False,
date_value=False):
for item in items:
- key, predicate, fallbacks, _type = item
+ try:
+ key, predicate, fallbacks, _type, _class = item
+ except ValueError:
+ key, predicate, fallbacks, _type = item
+ _class=None
self._add_triple_from_dict(_dict, subject, predicate, key,
fallbacks=fallbacks,
list_value=list_value,
date_value=date_value,
- _type=_type)
+ _type=_type,
+ _class=_class)
def _add_triple_from_dict(self, _dict, subject, predicate, key,
fallbacks=None,
@@ -740,7 +747,8 @@ def _add_triple_from_dict(self, _dict, subject, predicate, key,
date_value=False,
_type=Literal,
_datatype=None,
- value_modifier=None):
+ value_modifier=None,
+ _class=None):
'''
Adds a new triple to the graph with the provided parameters
@@ -756,6 +764,8 @@ def _add_triple_from_dict(self, _dict, subject, predicate, key,
If `list_value` or `date_value` are True, then the value is treated as
a list or a date respectively (see `_add_list_triple` and
`_add_date_triple` for details.
+
+ `_class` is the optional RDF class of the entity being added.
'''
value = self._get_dict_value(_dict, key)
if not value and fallbacks:
@@ -769,7 +779,7 @@ def _add_triple_from_dict(self, _dict, subject, predicate, key,
value = value_modifier(value)
if value and list_value:
- self._add_list_triple(subject, predicate, value, _type, _datatype)
+ self._add_list_triple(subject, predicate, value, _type, _datatype, _class=_class)
elif value and date_value:
self._add_date_triple(subject, predicate, value, _type)
elif value:
@@ -782,8 +792,10 @@ def _add_triple_from_dict(self, _dict, subject, predicate, key,
else:
object = _type(value)
self.g.add((subject, predicate, object))
+ if _class:
+ self.g.add((object, RDF.type, _class))
- def _add_list_triple(self, subject, predicate, value, _type=Literal, _datatype=None):
+ def _add_list_triple(self, subject, predicate, value, _type=Literal, _datatype=None, _class=None):
'''
Adds as many triples to the graph as values
@@ -802,8 +814,10 @@ def _add_list_triple(self, subject, predicate, value, _type=Literal, _datatype=N
else:
object = _type(item)
self.g.add((subject, predicate, object))
+ if _class:
+ self.g.add((object, RDF.type, _class))
- def _add_date_triple(self, subject, predicate, value, _type=Literal):
+ def _add_date_triple(self, subject, predicate, value, _type=Literal): #UNDONE -- add class here??, looks like we're already possibly tagging?
'''
Adds a new triple with a date object
@@ -908,6 +922,40 @@ def _get_or_create_spatial_ref(self, dataset_dict, dataset_ref):
self.g.add((dataset_ref, DCT.spatial, spatial_ref))
return spatial_ref
+ def _add_from_codelist(self, _dict, subject, predicate, key,
+ codelist,
+ _type=URIRefOrLiteral,
+ list_value=False,
+ fallbacks=False,
+ value_modifier=False):
+ ''' Add an item from a codelist, stored in rdf in the codelists directory '''
+
+ value = self._get_dict_value(_dict, key)
+ if not value and fallbacks:
+ for fallback in fallbacks:
+ value = self._get_dict_value(_dict, fallback)
+ if value:
+ break
+
+ if value and callable(value_modifier):
+ value = value_modifier(value)
+
+ def add(item):
+ ref = _type(item)
+ self.g.add((ref, RDF.type, SKOS.Concept))
+ self.g.add((ref, SKOS.inScheme, URIRef(codelist.scheme)))
+ self.g.add((subject, predicate, ref))
+ for lang, label in codelist.labels(item).items():
+ _label_ref = Literal(label, lang=lang)
+ self.g.add((ref, SKOS.prefLabel, _label_ref))
+
+ if list_value:
+ items = self._read_list_value(value)
+ for item in items:
+ add(item)
+ else:
+ add(value)
+
# Public methods for profiles to implement
def parse_dataset(self, dataset_dict, dataset_ref):
@@ -1355,103 +1403,110 @@ def graph_from_dataset(self, dataset_dict, dataset_ref):
# Resources
for resource_dict in dataset_dict.get('resources', []):
+ self.graph_from_resource(g, dataset_ref, resource_dict, resource_license_fallback)
+ def graph_from_resource(self, g, dataset_ref, resource_dict, resource_license_fallback, distribution=None):
+ if distribution is None:
distribution = CleanedURIRef(resource_uri(resource_dict))
- g.add((dataset_ref, DCAT.distribution, distribution))
+ g.add((dataset_ref, DCAT.distribution, distribution))
- g.add((distribution, RDF.type, DCAT.Distribution))
+ g.add((distribution, RDF.type, DCAT.Distribution))
- # Simple values
- items = [
- ('name', DCT.title, None, Literal),
- ('description', DCT.description, None, Literal),
- ('status', ADMS.status, None, URIRefOrLiteral),
- ('rights', DCT.rights, None, URIRefOrLiteral),
- ('license', DCT.license, None, URIRefOrLiteral),
- ('access_url', DCAT.accessURL, None, URIRef),
- ('download_url', DCAT.downloadURL, None, URIRef),
- ]
+ # Simple values
+ items = [
+ ('name', DCT.title, None, Literal),
+ ('description', DCT.description, None, Literal),
+ ('status', ADMS.status, None, URIRefOrLiteral),
+ ('rights', DCT.rights, None, URIRefOrLiteral),
+ ('license', DCT.license, None, URIRefOrLiteral, DCT.LicenseDocument),
+ ('access_url', DCAT.accessURL, None, URIRef),
+ ('download_url', DCAT.downloadURL, None, URIRef),
+ ]
- self._add_triples_from_dict(resource_dict, distribution, items)
+ self._add_triples_from_dict(resource_dict, distribution, items)
- # Lists
- items = [
- ('documentation', FOAF.page, None, URIRefOrLiteral),
- ('language', DCT.language, None, URIRefOrLiteral),
- ('conforms_to', DCT.conformsTo, None, Literal),
- ]
- self._add_list_triples_from_dict(resource_dict, distribution, items)
-
- # Set default license for distribution if needed and available
- if resource_license_fallback and not (distribution, DCT.license, None) in g:
- g.add((distribution, DCT.license, URIRefOrLiteral(resource_license_fallback)))
-
- # Format
- mimetype = resource_dict.get('mimetype')
- fmt = resource_dict.get('format')
-
- # IANA media types (either URI or Literal) should be mapped as mediaType.
- # In case format is available and mimetype is not set or identical to format,
- # check which type is appropriate.
- if fmt and (not mimetype or mimetype == fmt):
- if ('iana.org/assignments/media-types' in fmt
- or not fmt.startswith('http') and '/' in fmt):
- # output format value as dcat:mediaType instead of dct:format
- mimetype = fmt
- fmt = None
- else:
- # Use dct:format
- mimetype = None
+ # Lists
+ items = [
+ ('documentation', FOAF.page, None, URIRefOrLiteral),
+ ('language', DCT.language, None, URIRefOrLiteral),
+ ('conforms_to', DCT.conformsTo, None, Literal),
+ ]
+ self._add_list_triples_from_dict(resource_dict, distribution, items)
- if mimetype:
- g.add((distribution, DCAT.mediaType,
- URIRefOrLiteral(mimetype)))
+ # Set default license for distribution if needed and available
+ if resource_license_fallback and not (distribution, DCT.license, None) in g:
+ _ref = URIRefOrLiteral(resource_license_fallback)
+ g.add((_ref, RDF.type, DCT.LicenseDocument))
+ g.add((distribution, DCT.license, _ref))
- if fmt:
- g.add((distribution, DCT['format'],
- URIRefOrLiteral(fmt)))
+ # Format
+ mimetype = resource_dict.get('mimetype')
+ fmt = resource_dict.get('format')
+
+ # IANA media types (either URI or Literal) should be mapped as mediaType.
+ # In case format is available and mimetype is not set or identical to format,
+ # check which type is appropriate.
+ if fmt and (not mimetype or mimetype == fmt):
+ if ('iana.org/assignments/media-types' in fmt
+ or not fmt.startswith('http') and '/' in fmt):
+ # output format value as dcat:mediaType instead of dct:format
+ mimetype = fmt
+ fmt = None
+ else:
+ # Use dct:format
+ mimetype = None
+ if mimetype:
+ g.add((distribution, DCAT.mediaType,
+ URIRefOrLiteral(mimetype)))
- # URL fallback and old behavior
- url = resource_dict.get('url')
- download_url = resource_dict.get('download_url')
- access_url = resource_dict.get('access_url')
- # Use url as fallback for access_url if access_url is not set and download_url is not equal
- if url and not access_url:
- if (not download_url) or (download_url and url != download_url):
- self._add_triple_from_dict(resource_dict, distribution, DCAT.accessURL, 'url', _type=URIRef)
+ if fmt:
+ g.add((distribution, DCT['format'],
+ URIRefOrLiteral(fmt)))
- # Dates
- items = [
- ('issued', DCT.issued, ['created'], Literal),
- ('modified', DCT.modified, ['metadata_modified'], Literal),
- ]
- self._add_date_triples_from_dict(resource_dict, distribution, items)
+ # URL fallback and old behavior
+ url = resource_dict.get('url')
+ download_url = resource_dict.get('download_url')
+ access_url = resource_dict.get('access_url')
+ # Use url as fallback for access_url if access_url is not set and download_url is not equal
+ if url and not access_url:
+ if (not download_url) or (download_url and url != download_url):
+ self._add_triple_from_dict(resource_dict, distribution, DCAT.accessURL, 'url', _type=URIRef)
- # Numbers
- if resource_dict.get('size'):
- try:
- g.add((distribution, DCAT.byteSize,
- Literal(float(resource_dict['size']),
- datatype=XSD.decimal)))
- except (ValueError, TypeError):
- g.add((distribution, DCAT.byteSize,
- Literal(resource_dict['size'])))
- # Checksum
- if resource_dict.get('hash'):
- checksum = BNode()
- g.add((checksum, RDF.type, SPDX.Checksum))
- g.add((checksum, SPDX.checksumValue,
- Literal(resource_dict['hash'],
- datatype=XSD.hexBinary)))
+ # Dates
+ items = [
+ ('issued', DCT.issued, ['created'], Literal),
+ ('modified', DCT.modified, ['metadata_modified'], Literal),
+ ]
- if resource_dict.get('hash_algorithm'):
- g.add((checksum, SPDX.algorithm,
- URIRefOrLiteral(resource_dict['hash_algorithm'])))
+ self._add_date_triples_from_dict(resource_dict, distribution, items)
- g.add((distribution, SPDX.checksum, checksum))
+ # Numbers
+ if resource_dict.get('size'):
+ try:
+ g.add((distribution, DCAT.byteSize,
+ Literal(float(resource_dict['size']),
+ datatype=XSD.decimal)))
+ except (ValueError, TypeError):
+ g.add((distribution, DCAT.byteSize,
+ Literal(resource_dict['size'])))
+ # Checksum
+ if resource_dict.get('hash'):
+ checksum = BNode()
+ g.add((checksum, RDF.type, SPDX.Checksum))
+ g.add((checksum, SPDX.checksumValue,
+ Literal(resource_dict['hash'],
+ datatype=XSD.hexBinary)))
+
+ if resource_dict.get('hash_algorithm'):
+ g.add((checksum, SPDX.algorithm,
+ URIRefOrLiteral(resource_dict['hash_algorithm'])))
+
+ g.add((distribution, SPDX.checksum, checksum))
+
+ return distribution
def graph_from_catalog(self, catalog_dict, catalog_ref):
@@ -1595,22 +1650,25 @@ def parse_dataset(self, dataset_dict, dataset_ref):
resource_dict['access_services'] = json.dumps(access_service_list)
return dataset_dict
-
+
def graph_from_dataset(self, dataset_dict, dataset_ref):
# call super method
super(EuropeanDCATAP2Profile, self).graph_from_dataset(dataset_dict, dataset_ref)
# Lists
- for key, predicate, fallbacks, type, datatype in (
- ('temporal_resolution', DCAT.temporalResolution, None, Literal, XSD.duration),
- ('is_referenced_by', DCT.isReferencedBy, None, URIRefOrLiteral, None),
- ('applicable_legislation', DCATAP.applicableLegislation, None, URIRefOrLiteral, None),
- ('hvd_category', DCATAP.hvdCategory, None, URIRefOrLiteral, None),
+ for key, predicate, fallbacks, _type, datatype, _class in (
+ ('temporal_resolution', DCAT.temporalResolution, None, Literal, XSD.duration, None),
+ ('is_referenced_by', DCT.isReferencedBy, None, URIRefOrLiteral, None, None),
+ ('applicable_legislation', DCATAP.applicableLegislation, None, URIRefOrLiteral, None, ELI.LegalResource),
):
self._add_triple_from_dict(dataset_dict, dataset_ref, predicate, key, list_value=True,
- fallbacks=fallbacks, _type=type, _datatype=datatype)
-
+ fallbacks=fallbacks, _type=_type, _datatype=datatype, _class=_class)
+
+ self._add_from_codelist(dataset_dict, dataset_ref, DCATAP.hvdCategory, 'hvd_category',
+ codelists.high_value_dataset_category,
+ list_value=True)
+
# Temporal
start = self._get_dataset_value(dataset_dict, 'temporal_start')
end = self._get_dataset_value(dataset_dict, 'temporal_end')
@@ -1648,67 +1706,67 @@ def graph_from_dataset(self, dataset_dict, dataset_ref):
except (ValueError, TypeError):
self.g.add((dataset_ref, DCAT.spatialResolutionInMeters, Literal(value)))
- # Resources
- for resource_dict in dataset_dict.get('resources', []):
- distribution = CleanedURIRef(resource_uri(resource_dict))
+ def graph_from_resource(self, g, dataset_ref, resource_dict, resource_license_fallback, distribution=None):
+ distribution = super().graph_from_resource(g, dataset_ref, resource_dict, resource_license_fallback, distribution)
- # Simple values
- items = [
- ('availability', DCATAP.availability, None, URIRefOrLiteral),
- ('compress_format', DCAT.compressFormat, None, URIRefOrLiteral),
- ('package_format', DCAT.packageFormat, None, URIRefOrLiteral)
- ]
+ # Simple values
+ items = [
+ ('availability', DCATAP.availability, None, URIRefOrLiteral),
+ ('compress_format', DCAT.compressFormat, None, URIRefOrLiteral),
+ ('package_format', DCAT.packageFormat, None, URIRefOrLiteral)
+ ]
- self._add_triples_from_dict(resource_dict, distribution, items)
+ self._add_triples_from_dict(resource_dict, distribution, items)
- # Lists
- items = [
- ('applicable_legislation', DCATAP.applicableLegislation, None, URIRefOrLiteral),
- ]
- self._add_list_triples_from_dict(resource_dict, distribution, items)
+ # Lists
+ items = [
+ ('applicable_legislation', DCATAP.applicableLegislation, None, URIRefOrLiteral),
+ ]
+ self._add_list_triples_from_dict(resource_dict, distribution, items)
- try:
- access_service_list = json.loads(resource_dict.get('access_services', '[]'))
- # Access service
- for access_service_dict in access_service_list:
-
- access_service_uri = access_service_dict.get('uri')
- if access_service_uri:
- access_service_node = CleanedURIRef(access_service_uri)
- else:
- access_service_node = BNode()
- # Remember the (internal) access service reference for referencing in
- # further profiles
- access_service_dict['access_service_ref'] = str(access_service_node)
-
- self.g.add((distribution, DCAT.accessService, access_service_node))
-
- self.g.add((access_service_node, RDF.type, DCAT.DataService))
-
- # Simple values
- items = [
- ('availability', DCATAP.availability, None, URIRefOrLiteral),
- ('license', DCT.license, None, URIRefOrLiteral),
- ('access_rights', DCT.accessRights, None, URIRefOrLiteral),
- ('title', DCT.title, None, Literal),
- ('endpoint_description', DCAT.endpointDescription, None, Literal),
- ('description', DCT.description, None, Literal),
- ]
-
- self._add_triples_from_dict(access_service_dict, access_service_node, items)
+ try:
+ access_service_list = json.loads(resource_dict.get('access_services', '[]'))
+ # Access service
+ for access_service_dict in access_service_list:
- # Lists
- items = [
- ('endpoint_url', DCAT.endpointURL, None, URIRefOrLiteral),
- ('serves_dataset', DCAT.servesDataset, None, URIRefOrLiteral),
- ]
- self._add_list_triples_from_dict(access_service_dict, access_service_node, items)
-
- if access_service_list:
- resource_dict['access_services'] = json.dumps(access_service_list)
- except ValueError:
- pass
+ access_service_uri = access_service_dict.get('uri')
+ if access_service_uri:
+ access_service_node = CleanedURIRef(access_service_uri)
+ else:
+ access_service_node = BNode()
+ # Remember the (internal) access service reference for referencing in
+ # further profiles
+ access_service_dict['access_service_ref'] = str(access_service_node)
+
+ self.g.add((distribution, DCAT.accessService, access_service_node))
+
+ self.g.add((access_service_node, RDF.type, DCAT.DataService))
+
+ # Simple values
+ items = [
+ ('availability', DCATAP.availability, None, URIRefOrLiteral),
+ ('license', DCT.license, None, URIRefOrLiteral),
+ ('access_rights', DCT.accessRights, None, URIRefOrLiteral),
+ ('title', DCT.title, None, Literal),
+ ('endpoint_description', DCAT.endpointDescription, None, Literal),
+ ('description', DCT.description, None, Literal),
+ ]
+
+ self._add_triples_from_dict(access_service_dict, access_service_node, items)
+
+ # Lists
+ items = [
+ ('endpoint_url', DCAT.endpointURL, None, URIRefOrLiteral),
+ ('serves_dataset', DCAT.servesDataset, None, URIRefOrLiteral),
+ ]
+ self._add_list_triples_from_dict(access_service_dict, access_service_node, items)
+
+ if access_service_list:
+ resource_dict['access_services'] = json.dumps(access_service_list)
+ except ValueError:
+ pass
+ return distribution
def graph_from_catalog(self, catalog_dict, catalog_ref):
@@ -1716,6 +1774,14 @@ def graph_from_catalog(self, catalog_dict, catalog_ref):
super(EuropeanDCATAP2Profile, self).graph_from_catalog(catalog_dict, catalog_ref)
+class EuropeanDCATAPHVD220Profile(EuropeanDCATAP2Profile):
+ ''' Read only catalog output for the HVD Profile, that only includes HVD resources '''
+ def graph_from_resource(self, g, dataset_ref, resource_dict, resource_license_fallback, distribution=None):
+ if 'http://data.europa.eu/eli/reg_impl/2023/138/oj' in \
+ self._read_list_value(self._get_dataset_value(resource_dict, 'applicable_legislation')):
+ return super().graph_from_resource(g, dataset_ref, resource_dict, resource_license_fallback, distribution)
+
+
class SchemaOrgProfile(RDFProfile):
'''
An RDF profile based on the schema.org Dataset
diff --git a/ckanext/dcat/tests/shaql/README.md b/ckanext/dcat/tests/shaql/README.md
new file mode 100644
index 00000000..9fb26dc9
--- /dev/null
+++ b/ckanext/dcat/tests/shaql/README.md
@@ -0,0 +1 @@
+These directories have SHAQL shapes to validate the DCAT compliance.
\ No newline at end of file
diff --git a/ckanext/dcat/tests/shaql/hvd/LICENSE.md b/ckanext/dcat/tests/shaql/hvd/LICENSE.md
new file mode 100644
index 00000000..706653c9
--- /dev/null
+++ b/ckanext/dcat/tests/shaql/hvd/LICENSE.md
@@ -0,0 +1,5 @@
+This directory was retrieved from https://github.com/SEMICeu/DCAT-AP/
+
+Licence from Upstream:
+Copyright © 2023 European Union. All material in this repository (https://github.com/SEMICeu/DCAT-AP/) is published under the licence CC-BY 4.0, unless explicitly otherwise mentioned.
+
diff --git a/ckanext/dcat/tests/shaql/hvd/README.md b/ckanext/dcat/tests/shaql/hvd/README.md
new file mode 100644
index 00000000..4809da85
--- /dev/null
+++ b/ckanext/dcat/tests/shaql/hvd/README.md
@@ -0,0 +1,4 @@
+https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/html/hvd-SHACL-base.ttl
+https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/html/hvd-SHACL-ranges.ttl
+https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/html/hvd-SHACL-full.ttl
+
diff --git a/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-base.ttl b/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-base.ttl
new file mode 100644
index 00000000..7cadcc13
--- /dev/null
+++ b/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-base.ttl
@@ -0,0 +1,566 @@
+@prefix dc: .
+@prefix dcat: .
+@prefix foaf: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix shacl: .
+@prefix skos: .
+@prefix vcard: .
+@prefix xsd: .
+
+ rdfs:member ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ;
+ shacl:targetClass dcat:CatalogRecord .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:name "primary topic"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path foaf:primaryTopic;
+ "The expected value for primary topic is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:minCount 1;
+ shacl:name "primary topic"@en;
+ shacl:path foaf:primaryTopic;
+ "Minimally 1 values are expected for primary topic"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:maxCount 1;
+ shacl:name "primary topic"@en;
+ shacl:path foaf:primaryTopic;
+ "Maximally 1 values allowed for primary topic"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ;
+ shacl:targetClass dcat:Catalog .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Catalogue.dataset";
+ shacl:description "A Dataset that is part of the Catalogue."@en;
+ shacl:name "dataset"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:dataset;
+ "The expected value for dataset is a rdfs:Resource (URI or blank node)"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Catalogue.record";
+ shacl:description "A Catalogue Record that is part of the Catalogue"@en;
+ shacl:name "record"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:record;
+ "The expected value for record is a rdfs:Resource (URI or blank node)"@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Catalogue.service";
+ shacl:description "A site or end-point (Data Service) that is listed in the Catalogue."@en;
+ shacl:name "service"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:service;
+ "The expected value for service is a rdfs:Resource (URI or blank node)"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass dcat:Resource .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass skos:Concept .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ;
+ shacl:targetClass dcat:DataService .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Data Service."@en;
+ shacl:name "applicable legislation"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path ;
+ "The expected value for applicable legislation is a rdfs:Resource (URI or blank node)"@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.endpointURL";
+ shacl:description "The root location or primary endpoint of the service (an IRI)."@en;
+ shacl:name "endpoint URL"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:endpointURL;
+ "The expected value for endpoint URL is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.HVDcategory";
+ shacl:description "The HVD category to which this Data Service belongs."@en;
+ shacl:name "HVD category"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path ;
+ "The expected value for HVD category is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.endpointdescription";
+ shacl:description "A description of the services available via the end-points, including their operations, parameters etc."@en;
+ shacl:name "endpoint description"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:endpointDescription;
+ "The expected value for endpoint description is a rdfs:Resource (URI or blank node)"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.contactpoint";
+ shacl:description "Contact information that can be used for sending comments about the Data Service."@en;
+ shacl:minCount 1;
+ shacl:name "contact point"@en;
+ shacl:path dcat:contactPoint;
+ "Minimally 1 values are expected for contact point"@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.endpointURL";
+ shacl:description "The root location or primary endpoint of the service (an IRI)."@en;
+ shacl:minCount 1;
+ shacl:name "endpoint URL"@en;
+ shacl:path dcat:endpointURL;
+ "Minimally 1 values are expected for endpoint URL"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.documentation";
+ shacl:description "A page that provides additional information about the Data Service."@en;
+ shacl:minCount 1;
+ shacl:name "documentation"@en;
+ shacl:path foaf:Page;
+ "Minimally 1 values are expected for documentation"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.documentation";
+ shacl:description "A page that provides additional information about the Data Service."@en;
+ shacl:name "documentation"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path foaf:Page;
+ "The expected value for documentation is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.contactpoint";
+ shacl:description "Contact information that can be used for sending comments about the Data Service."@en;
+ shacl:name "contact point"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:contactPoint;
+ "The expected value for contact point is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.licence";
+ shacl:description "A licence under which the Data service is made available."@en;
+ shacl:maxCount 1;
+ shacl:name "licence"@en;
+ shacl:path dc:license;
+ "Maximally 1 values allowed for licence"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.rights";
+ shacl:description "A statement that specifies rights associated with the Distribution."@en;
+ shacl:name "rights"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:rights;
+ "The expected value for rights is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.servesdataset";
+ shacl:description "This property refers to a collection of data that this data service can distribute."@en;
+ shacl:name "serves dataset"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:servesDataset;
+ "The expected value for serves dataset is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Data Service."@en;
+ shacl:minCount 1;
+ shacl:name "applicable legislation"@en;
+ shacl:path ;
+ "Minimally 1 values are expected for applicable legislation"@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.servesdataset";
+ shacl:description "This property refers to a collection of data that this data service can distribute."@en;
+ shacl:minCount 1;
+ shacl:name "serves dataset"@en;
+ shacl:path dcat:servesDataset;
+ "Minimally 1 values are expected for serves dataset"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.licence";
+ shacl:description "A licence under which the Data service is made available."@en;
+ shacl:name "licence"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:license;
+ "The expected value for licence is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.HVDcategory";
+ shacl:description "The HVD category to which this Data Service belongs."@en;
+ shacl:minCount 1;
+ shacl:name "HVD category"@en;
+ shacl:path ;
+ "Minimally 1 values are expected for HVD category"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ;
+ shacl:targetClass dcat:Dataset .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.conformsto";
+ shacl:description "An implementing rule or other specification."@en;
+ shacl:name "conforms to"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:conformsTo;
+ "The expected value for conforms to is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Dataset."@en;
+ shacl:name "applicable legislation"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path ;
+ "The expected value for applicable legislation is a rdfs:Resource (URI or blank node)"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.datasetdistribution";
+ shacl:description "An available Distribution for the Dataset."@en;
+ shacl:minCount 1;
+ shacl:name "dataset distribution"@en;
+ shacl:path dcat:distribution;
+ "Minimally 1 values are expected for dataset distribution"@en .
+
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.HVDCategory";
+ shacl:description "The HVD category to which this Dataset belongs."@en;
+ shacl:name "HVD Category"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path ;
+ "The expected value for HVD Category is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.contactpoint";
+ shacl:description "Contact information that can be used for sending comments about the Dataset."@en;
+ shacl:name "contact point"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:contactPoint;
+ "The expected value for contact point is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.HVDCategory";
+ shacl:description "The HVD category to which this Dataset belongs."@en;
+ shacl:minCount 1;
+ shacl:name "HVD Category"@en;
+ shacl:path ;
+ "Minimally 1 values are expected for HVD Category"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Dataset."@en;
+ shacl:minCount 1;
+ shacl:name "applicable legislation"@en;
+ shacl:path ;
+ "Minimally 1 values are expected for applicable legislation"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.datasetdistribution";
+ shacl:description "An available Distribution for the Dataset."@en;
+ shacl:name "dataset distribution"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:distribution;
+ "The expected value for dataset distribution is a rdfs:Resource (URI or blank node)"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ;
+ shacl:targetClass dcat:Distribution .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Distribution"@en;
+ shacl:name "applicable legislation"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path ;
+ "The expected value for applicable legislation is a rdfs:Resource (URI or blank node)"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.accessservice";
+ shacl:description "A data service that gives access to the distribution of the dataset"@en;
+ shacl:name "access service"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:accessService;
+ "The expected value for access service is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.linkedschemas";
+ shacl:description "An established schema to which the described Distribution conforms."@en;
+ shacl:name "linked schemas"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:conformsTo;
+ "The expected value for linked schemas is a rdfs:Resource (URI or blank node)"@en .
+
+
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.licence";
+ shacl:description "A licence under which the Distribution is made available."@en;
+ shacl:maxCount 1;
+ shacl:name "licence"@en;
+ shacl:path dc:license;
+ "Maximally 1 values allowed for licence"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.accessURL";
+ shacl:description "A URL that gives access to a Distribution of the Dataset."@en;
+ shacl:name "access URL"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dcat:accessURL;
+ "The expected value for access URL is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.rights";
+ shacl:description "A statement that specifies rights associated with the Distribution."@en;
+ shacl:name "rights"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:rights;
+ "The expected value for rights is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.applicablelegislation";
+ shacl:description "The legislation that mandates the creation or management of the Distribution"@en;
+ shacl:minCount 1;
+ shacl:name "applicable legislation"@en;
+ shacl:path ;
+ "Minimally 1 values are expected for applicable legislation"@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.accessURL";
+ shacl:description "A URL that gives access to a Distribution of the Dataset."@en;
+ shacl:minCount 1;
+ shacl:name "access URL"@en;
+ shacl:path dcat:accessURL;
+ "Minimally 1 values are expected for access URL"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.licence";
+ shacl:description "A licence under which the Distribution is made available."@en;
+ shacl:name "licence"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path dc:license;
+ "The expected value for licence is a rdfs:Resource (URI or blank node)"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass foaf:Document .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property
+ ,
+ ,
+ ,
+ ;
+ shacl:targetClass vcard:Kind .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Kind.email";
+ shacl:description """A email address via which contact can be made."""@en;
+ shacl:name "email"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path vcard:hasEmail;
+ "The expected value for email is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Kind.contactpage";
+ shacl:description "A webpage that either allows to make contact (i.e. a webform) or the information contains how to get into contact. "@en;
+ shacl:name "contact page"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path vcard:hasURL;
+ "The expected value for contact page is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Kind.contactpage";
+ shacl:description "A webpage that either allows to make contact (i.e. a webform) or the information contains how to get into contact. "@en;
+ shacl:maxCount 1;
+ shacl:name "contact page"@en;
+ shacl:path vcard:hasURL;
+ "Maximally 1 values allowed for contact page"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Kind.email";
+ shacl:description """A email address via which contact can be made."""@en;
+ shacl:maxCount 1;
+ shacl:name "email"@en;
+ shacl:path vcard:hasEmail;
+ "Maximally 1 values allowed for email"@en .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass dc:LicenseDocument .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass rdfs:Literal .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass rdfs:Resource .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass dc:RightsStatement .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:targetClass dc:Standard .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io/uri.semic.eu-generated/DCAT-AP/releases/2.2.0-hvd/#Kind";
+ shacl:description """It is recommended to provide at least either an email or a contact form from e.g. a service desk. """@en;
+ shacl:or (
+ [
+ shacl:path vcard:hasEmail;
+ shacl:minCount 1 ;
+ ]
+ [
+ shacl:path vcard:hasURL;
+ shacl:minCount 1 ;
+ ]
+ ) ;
+ a shacl:NodeShape ;
+ shacl:targetClass vcard:Kind ;
+ shacl:severity shacl:Warning ;
+ "It is recommended to provide at least either an email or a contact form from e.g. a service desk. "@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io/uri.semic.eu-generated/DCAT-AP/releases/2.2.0-hvd/#c3";
+ shacl:description """It is mandatory to provide legal information."""@en;
+ shacl:or (
+ [
+ shacl:path dc:license;
+ shacl:minCount 1 ;
+ ]
+ [
+ shacl:path dc:rights;
+ shacl:minCount 1 ;
+ ]
+ ) ;
+ a shacl:NodeShape ;
+ shacl:targetClass dcat:Distribution;
+ "It is mandatory to provide legal information."@en .
+
+ rdfs:seeAlso "https://semiceu.github.io/uri.semic.eu-generated/DCAT-AP/releases/2.2.0-hvd/#c3";
+ shacl:description """It is mandatory to provide legal information."""@en;
+ shacl:or (
+ [
+ shacl:path dc:license;
+ shacl:minCount 1 ;
+ ]
+ [
+ shacl:path dc:rights;
+ shacl:minCount 1 ;
+ ]
+ ) ;
+ a shacl:NodeShape ;
+ shacl:targetClass dcat:DataService;
+ "It is mandatory to provide legal information."@en .
+
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.applicablelegislation";
+ shacl:description "The applicable legislation must be set to the HVD IR ELI."@en;
+ shacl:path ;
+ shacl:hasValue ;
+ "The applicable legislation must be set to the HVD IR ELI."@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.applicablelegislation";
+ shacl:description "The applicable legislation must be set to the HVD IR ELI."@en;
+ shacl:path ;
+ shacl:hasValue ;
+ "The applicable legislation must be set to the HVD IR ELI."@en .
+
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Distribution.applicablelegislation";
+ shacl:description "The applicable legislation must be set to the HVD IR ELI."@en;
+ shacl:path ;
+ shacl:hasValue ;
+ "The applicable legislation must be set to the HVD IR ELI."@en .
+
+
+ rdf:type owl:Ontology ;
+ owl:imports .
+
+
+ a shacl:NodeShape ;
+ rdfs:comment "HVD Category Restriction" ;
+ rdfs:label "HVD Category Restriction" ;
+ shacl:property [
+ shacl:hasValue ;
+ shacl:minCount 1 ;
+ shacl:nodeKind shacl:IRI ;
+ shacl:path skos:inScheme
+ ] .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#Dataset.HVDcategory";
+ shacl:description "The HVD category to which this Dataset belongs."@en;
+ shacl:name "HVD category"@en;
+ shacl:path ;
+ shacl:node ;
+ "The range of HVD category must be of type ."@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#DataService.HVDcategory";
+ shacl:description "The HVD category to which this Data Service belongs."@en;
+ shacl:name "HVD category"@en;
+ shacl:path ;
+ shacl:node ;
+ "The range of HVD category must be of type ."@en .
diff --git a/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-full.ttl b/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-full.ttl
new file mode 100644
index 00000000..b5bf7b9b
--- /dev/null
+++ b/ckanext/dcat/tests/shaql/hvd/hvd-SHACL-full.ttl
@@ -0,0 +1,712 @@
+@prefix dc: .
+@prefix dcat: .
+@prefix foaf: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix shacl: .
+@prefix skos: .
+@prefix vcard: .
+@prefix xsd: .
+
+ rdfs:member ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ .
+
+ a shacl:NodeShape;
+ shacl:closed false;
+ shacl:property ,
+ ,
+ ,
+ ;
+ shacl:targetClass dcat:CatalogRecord .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:name "primary topic"@en;
+ shacl:nodeKind shacl:BlankNodeOrIRI;
+ shacl:path foaf:primaryTopic;
+ "The expected value for primary topic is a rdfs:Resource (URI or blank node)"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:minCount 1;
+ shacl:name "primary topic"@en;
+ shacl:path foaf:primaryTopic;
+ "Minimally 1 values are expected for primary topic"@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:class dcat:Resource;
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:name "primary topic"@en;
+ shacl:path foaf:primaryTopic;
+ "The range of primary topic must be of type ."@en .
+
+ rdfs:seeAlso "https://semiceu.github.io//DCAT-AP/releases/2.2.0-hvd#CatalogueRecord.primarytopic";
+ shacl:description "A link to the Dataset, Data service or Catalog described in the record."@en;
+ shacl:maxCount 1;
+ shacl:name "primary topic"@en;
+ shacl:path foaf:primaryTopic;
+ "Maximally 1 values allowed for primary topic"@en .
+
+