From 1dd88f80d60f8e7de212f7a53d41804069e9cd25 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 22 Aug 2024 11:12:09 +0200 Subject: [PATCH 01/14] Initial DCAT AP 3 profile, schema and test --- ckanext/dcat/profiles/__init__.py | 1 + ckanext/dcat/profiles/euro_dcat_ap_3.py | 8 + ckanext/dcat/schemas/dcat_ap_3_full.yaml | 384 +++ .../tests/shacl/dcat-ap_3_shacl_shapes.ttl | 2321 +++++++++++++++++ ckanext/dcat/tests/test_shacl.py | 31 + setup.py | 1 + 6 files changed, 2746 insertions(+) create mode 100644 ckanext/dcat/profiles/euro_dcat_ap_3.py create mode 100644 ckanext/dcat/schemas/dcat_ap_3_full.yaml create mode 100644 ckanext/dcat/tests/shacl/dcat-ap_3_shacl_shapes.ttl diff --git a/ckanext/dcat/profiles/__init__.py b/ckanext/dcat/profiles/__init__.py index a80a48c6..29bb0e78 100644 --- a/ckanext/dcat/profiles/__init__.py +++ b/ckanext/dcat/profiles/__init__.py @@ -20,5 +20,6 @@ from .euro_dcat_ap import EuropeanDCATAPProfile from .euro_dcat_ap_2 import EuropeanDCATAP2Profile +from .euro_dcat_ap_3 import EuropeanDCATAP3Profile from .euro_dcat_ap_scheming import EuropeanDCATAPSchemingProfile from .schemaorg import SchemaOrgProfile diff --git a/ckanext/dcat/profiles/euro_dcat_ap_3.py b/ckanext/dcat/profiles/euro_dcat_ap_3.py new file mode 100644 index 00000000..e4fbd1a6 --- /dev/null +++ b/ckanext/dcat/profiles/euro_dcat_ap_3.py @@ -0,0 +1,8 @@ +from ckanext.dcat.profiles import EuropeanDCATAP2Profile + + +class EuropeanDCATAP3Profile(EuropeanDCATAP2Profile): + """ + An RDF profile based on the DCAT-AP 3 for data portals in Europe + """ + diff --git a/ckanext/dcat/schemas/dcat_ap_3_full.yaml b/ckanext/dcat/schemas/dcat_ap_3_full.yaml new file mode 100644 index 00000000..8f9f4afc --- /dev/null +++ b/ckanext/dcat/schemas/dcat_ap_3_full.yaml @@ -0,0 +1,384 @@ +scheming_version: 2 +dataset_type: dataset +about: Full DCAT AP 2.1 schema +about_url: http://github.com/ckan/ckanext-dcat + +dataset_fields: + +- field_name: title + label: Title + preset: title + required: true + help_text: A descriptive title for the dataset. + +- field_name: name + label: URL + preset: dataset_slug + form_placeholder: eg. my-dataset + +- field_name: notes + label: Description + required: true + form_snippet: markdown.html + help_text: A free-text account of the dataset. + +- field_name: tag_string + label: Keywords + preset: tag_string_autocomplete + form_placeholder: eg. economy, mental health, government + help_text: Keywords or tags describing the dataset. Use commas to separate multiple values. + +- field_name: contact + label: Contact points + repeating_label: Contact point + repeating_subfields: + + - field_name: uri + label: URI + + - field_name: name + label: Name + + - field_name: email + label: Email + display_snippet: email.html + help_text: Contact information for enquiries about the dataset. + +- field_name: publisher + label: Publisher + repeating_label: Publisher + repeating_once: true + repeating_subfields: + + - field_name: uri + label: URI + + - field_name: name + label: Name + + - field_name: email + label: Email + display_snippet: email.html + + - field_name: url + label: URL + display_snippet: link.html + + - field_name: type + label: Type + help_text: Entity responsible for making the dataset available. + +- field_name: license_id + label: License + form_snippet: license.html + help_text: License definitions and additional information can be found at http://opendefinition.org/. + +- field_name: owner_org + label: Organization + preset: dataset_organization + help_text: The CKAN organization the dataset belongs to. + +- field_name: url + label: Landing page + form_placeholder: http://example.com/dataset.json + display_snippet: link.html + help_text: Web page that can be navigated to gain access to the dataset, its distributions and/or additional information. + + # Note: this will fall back to metadata_created if not present +- field_name: issued + label: Release date + preset: dcat_date + help_text: Date of publication of the dataset. + + # Note: this will fall back to metadata_modified if not present +- field_name: modified + label: Modification date + preset: dcat_date + help_text: Most recent date on which the dataset was changed, updated or modified. + +- field_name: version + label: Version + validators: ignore_missing unicode_safe package_version_validator + help_text: Version number or other version designation of the dataset. + +- field_name: version_notes + label: Version notes + validators: ignore_missing unicode_safe + form_snippet: markdown.html + display_snippet: markdown.html + help_text: A description of the differences between this version and a previous version of the dataset. + + # Note: CKAN will generate a unique identifier for each dataset +- field_name: identifier + label: Identifier + help_text: A unique identifier of the dataset. + +- field_name: frequency + label: Frequency + help_text: The frequency at which dataset is published. + +- field_name: provenance + label: Provenance + form_snippet: markdown.html + display_snippet: markdown.html + help_text: A statement about the lineage of the dataset. + +- field_name: dcat_type + label: Type + help_text: The type of the dataset. + # TODO: controlled vocabulary? + +- field_name: temporal_coverage + label: Temporal coverage + repeating_subfields: + + - field_name: start + label: Start + preset: dcat_date + + - field_name: end + label: End + preset: dcat_date + help_text: The temporal period or periods the dataset covers. + +- field_name: temporal_resolution + label: Temporal resolution + help_text: Minimum time period resolvable in the dataset. + +- field_name: spatial_coverage + label: Spatial coverage + repeating_subfields: + + - field_name: uri + label: URI + + - field_name: text + label: Label + + - field_name: geom + label: Geometry + + - field_name: bbox + label: Bounding Box + + - field_name: centroid + label: Centroid + help_text: A geographic region that is covered by the dataset. + +- field_name: spatial_resolution_in_meters + label: Spatial resolution in meters + help_text: Minimum spatial separation resolvable in a dataset, measured in meters. + +- field_name: access_rights + label: Access rights + validators: ignore_missing unicode_safe + form_snippet: markdown.html + display_snippet: markdown.html + help_text: Information that indicates whether the dataset is Open Data, has access restrictions or is not public. + +- field_name: alternate_identifier + label: Other identifier + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: This property refers to a secondary identifier of the dataset, such as MAST/ADS, DataCite, DOI, etc. + +- field_name: theme + label: Theme + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: A category of the dataset. A Dataset may be associated with multiple themes. + +- field_name: language + label: Language + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: Language or languages of the dataset. + # TODO: language form snippet / validator / graph + +- field_name: documentation + label: Documentation + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: A page or document about this dataset. + +- field_name: conforms_to + label: Conforms to + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: An implementing rule or other specification that the dataset follows. + +- field_name: is_referenced_by + label: Is referenced by + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: A related resource, such as a publication, that references, cites, or otherwise points to the dataset. + +- field_name: applicable_legislation + label: Applicable legislation + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: The legislation that mandates the creation or management of the dataset. + +#- field_name: hvd_category +# label: HVD Category +# preset: multiple_text +# validators: ignore_missing scheming_multiple_text +# TODO: implement separately as part of wider HVD support + +# Note: if not provided, this will be autogenerated +- field_name: uri + label: URI + help_text: An URI for this dataset (if not provided it will be autogenerated). + +# TODO: relation-based properties are not yet included (e.g. is_version_of, source, sample, etc) +# +resource_fields: + +- field_name: url + label: URL + preset: resource_url_upload + +- field_name: name + label: Name + form_placeholder: + help_text: A descriptive title for the resource. + +- field_name: description + label: Description + form_snippet: markdown.html + help_text: A free-text account of the resource. + +- field_name: format + label: Format + preset: resource_format_autocomplete + help_text: File format. If not provided it will be guessed. + +- field_name: mimetype + label: Media type + validators: if_empty_guess_format ignore_missing unicode_safe + help_text: Media type for this format. If not provided it will be guessed. + +- field_name: compress_format + label: Compress format + help_text: The format of the file in which the data is contained in a compressed form. + +- field_name: package_format + label: Package format + help_text: The format of the file in which one or more data files are grouped together. + +- field_name: size + label: Size + validators: ignore_missing int_validator + form_snippet: number.html + display_snippet: file_size.html + help_text: File size in bytes + +- field_name: hash + label: Hash + help_text: Checksum of the downloaded file. + +- field_name: hash_algorithm + label: Hash Algorithm + help_text: Algorithm used to calculate to checksum. + +- field_name: rights + label: Rights + form_snippet: markdown.html + display_snippet: markdown.html + help_text: Some statement about the rights associated with the resource. + +- field_name: availability + label: Availability + help_text: Indicates how long it is planned to keep the resource available. + +- field_name: status + label: Status + preset: select + choices: + - value: http://purl.org/adms/status/Completed + label: Completed + - value: http://purl.org/adms/status/UnderDevelopment + label: Under Development + - value: http://purl.org/adms/status/Deprecated + label: Deprecated + - value: http://purl.org/adms/status/Withdrawn + label: Withdrawn + help_text: The status of the resource in the context of maturity lifecycle. + +- field_name: license + label: License + help_text: License in which the resource is made available. If not provided will be inherited from the dataset. + + # Note: this falls back to the standard resource url field +- field_name: access_url + label: Access URL + help_text: URL that gives access to the dataset (defaults to the standard resource URL). + + # Note: this falls back to the standard resource url field +- field_name: download_url + label: Download URL + help_text: URL that provides a direct link to a downloadable file (defaults to the standard resource URL). + +- field_name: issued + label: Release date + preset: dcat_date + help_text: Date of publication of the resource. + +- field_name: modified + label: Modification date + preset: dcat_date + help_text: Most recent date on which the resource was changed, updated or modified. + +- field_name: language + label: Language + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: Language or languages of the resource. + +- field_name: documentation + label: Documentation + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: A page or document about this resource. + +- field_name: conforms_to + label: Conforms to + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: An established schema to which the described resource conforms. + +- field_name: applicable_legislation + label: Applicable legislation + preset: multiple_text + validators: ignore_missing scheming_multiple_text + help_text: The legislation that mandates the creation or management of the resource. + +- field_name: access_services + label: Access services + repeating_label: Access service + repeating_subfields: + + - field_name: uri + label: URI + + - field_name: title + label: Title + + - field_name: endpoint_description + label: Endpoint description + + - field_name: endpoint_url + label: Endpoint URL + preset: multiple_text + + - field_name: serves_dataset + label: Serves dataset + preset: multiple_text + validators: ignore_missing scheming_multiple_text + + help_text: A data service that gives access to the resource. + + # Note: if not provided, this will be autogenerated +- field_name: uri + label: URI + help_text: An URI for this resource (if not provided it will be autogenerated). diff --git a/ckanext/dcat/tests/shacl/dcat-ap_3_shacl_shapes.ttl b/ckanext/dcat/tests/shacl/dcat-ap_3_shacl_shapes.ttl new file mode 100644 index 00000000..1aecadfd --- /dev/null +++ b/ckanext/dcat/tests/shacl/dcat-ap_3_shacl_shapes.ttl @@ -0,0 +1,2321 @@ +@prefix dc: . +@prefix dcat: . +@prefix foaf: . +@prefix prov: . +@prefix rdf: . +@prefix rdfs: . +@prefix shacl: . +@prefix skos: . +@prefix vcard: . +@prefix xsd: . + + rdfs:member , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass rdfs:Literal . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Identifier.notation"; + shacl:description "A string that is an identifier in the context of the identifier scheme referenced by its datatype."@en; + shacl:name "notation"@en; + shacl:nodeKind shacl:Literal; + shacl:path skos:notation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Identifier.notation"; + shacl:description "A string that is an identifier in the context of the identifier scheme referenced by its datatype."@en; + shacl:minCount 1; + shacl:name "notation"@en; + shacl:path skos:notation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Identifier.notation"; + shacl:description "A string that is an identifier in the context of the identifier scheme referenced by its datatype."@en; + shacl:maxCount 1; + shacl:name "notation"@en; + shacl:path skos:notation . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:CatalogRecord . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.modificationdate"; + shacl:description "The most recent date on which the Catalogue entry was changed or modified."@en; + shacl:minCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.changetype"; + shacl:description "The status of the catalogue record in the context of editorial flow of the dataset and data service descriptions."@en; + shacl:maxCount 1; + shacl:name "change type"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.language"; + shacl:class dc:LinguisticSystem; + shacl:description "A language used in the textual metadata describing titles, descriptions, etc. of the Catalogued Resource."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.applicationprofile"; + shacl:class dc:Standard; + shacl:description "An Application Profile that the Catalogued Resource's metadata conforms to."@en; + shacl:name "application profile"@en; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.sourcemetadata"; + shacl:description "The original metadata that was used in creating metadata for the Dataset, Data Service or Dataset Series."@en; + shacl:maxCount 1; + shacl:name "source metadata"@en; + shacl:path dc:source . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.changetype"; + shacl:class skos:Concept; + shacl:description "The status of the catalogue record in the context of editorial flow of the dataset and data service descriptions."@en; + shacl:name "change type"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.listingdate"; + shacl:description "The date on which the description of the Resource was included in the Catalogue."@en; + shacl:maxCount 1; + shacl:name "listing date"@en; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.modificationdate"; + shacl:description "The most recent date on which the Catalogue entry was changed or modified."@en; + shacl:name "modification date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.description"; + shacl:description "A free-text account of the record. This property can be repeated for parallel language versions of the description."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.changetype"; + shacl:description "The status of the catalogue record in the context of editorial flow of the dataset and data service descriptions."@en; + shacl:name "change type"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.sourcemetadata"; + shacl:class dcat:CatalogRecord; + shacl:description "The original metadata that was used in creating metadata for the Dataset, Data Service or Dataset Series."@en; + shacl:name "source metadata"@en; + shacl:path dc:source . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.applicationprofile"; + shacl:description "An Application Profile that the Catalogued Resource's metadata conforms to."@en; + shacl:name "application profile"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.modificationdate"; + shacl:description "The most recent date on which the Catalogue entry was changed or modified."@en; + shacl:maxCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.sourcemetadata"; + shacl:description "The original metadata that was used in creating metadata for the Dataset, Data Service or Dataset Series."@en; + shacl:name "source metadata"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:source . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.listingdate"; + shacl:description "The date on which the description of the Resource was included in the Catalogue."@en; + shacl:name "listing date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.title"; + shacl:description "A name given to the Catalogue Record."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#CatalogueRecord.language"; + shacl:description "A language used in the textual metadata describing titles, descriptions, etc. of the Catalogued Resource."@en; + shacl:name "language"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:Catalog . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.geographicalcoverage"; + shacl:class dc:Location; + shacl:description "A geographical area covered by the Catalogue."@en; + shacl:name "geographical coverage"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.haspart"; + shacl:description "A related Catalogue that is part of the described Catalogue."@en; + shacl:name "has part"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:hasPart . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.themes"; + shacl:description "A knowledge organization system used to classify the Resources that are in the Catalogue."@en; + shacl:name "themes"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:themeTaxonomy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.applicablelegislation"; + shacl:description "The legislation that mandates the creation or management of the Catalog."@en; + shacl:name "applicable legislation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.language"; + shacl:class dc:LinguisticSystem; + shacl:description "A language used in the textual metadata describing titles, descriptions, etc. of the Datasets in the Catalogue."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.record"; + shacl:class dcat:CatalogRecord; + shacl:description "A Catalogue Record that is part of the Catalogue."@en; + shacl:name "record"@en; + shacl:path dcat:record . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.rights"; + shacl:description "A statement that specifies rights associated with the Catalogue."@en; + shacl:maxCount 1; + shacl:name "rights"@en; + shacl:path dc:rights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.catalogue"; + shacl:class dcat:Catalog; + shacl:description "A catalogue whose contents are of interest in the context of this catalogue."@en; + shacl:name "catalogue"@en; + shacl:path dcat:catalog . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Catalogue."@en; + shacl:name "release date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.themes"; + shacl:class skos:ConceptScheme; + shacl:description "A knowledge organization system used to classify the Resources that are in the Catalogue."@en; + shacl:name "themes"@en; + shacl:path dcat:themeTaxonomy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.publisher"; + shacl:description "An entity (organisation) responsible for making the Catalogue available."@en; + shacl:maxCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.modificationdate"; + shacl:description "The most recent date on which the Catalogue was modified."@en; + shacl:name "modification date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.description"; + shacl:description "A free-text account of the Catalogue."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.applicablelegislation"; + shacl:class ; + shacl:description "The legislation that mandates the creation or management of the Catalog."@en; + shacl:name "applicable legislation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.dataset"; + shacl:class dcat:Dataset; + shacl:description "A Dataset that is part of the Catalogue."@en; + shacl:name "dataset"@en; + shacl:path dcat:dataset . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.creator"; + shacl:description "An entity responsible for the creation of the catalogue."@en; + shacl:name "creator"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:creator . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.temporalcoverage"; + shacl:description "A temporal period that the Catalogue covers."@en; + shacl:name "temporal coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.creator"; + shacl:description "An entity responsible for the creation of the catalogue."@en; + shacl:maxCount 1; + shacl:name "creator"@en; + shacl:path dc:creator . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.homepage"; + shacl:description "A web page that acts as the main page for the Catalogue."@en; + shacl:name "homepage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path foaf:homepage . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.temporalcoverage"; + shacl:class dc:PeriodOfTime; + shacl:description "A temporal period that the Catalogue covers."@en; + shacl:name "temporal coverage"@en; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.description"; + shacl:description "A free-text account of the Catalogue."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.publisher"; + shacl:description "An entity (organisation) responsible for making the Catalogue available."@en; + shacl:name "publisher"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.geographicalcoverage"; + shacl:description "A geographical area covered by the Catalogue."@en; + shacl:name "geographical coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.rights"; + shacl:class dc:RightsStatement; + shacl:description "A statement that specifies rights associated with the Catalogue."@en; + shacl:name "rights"@en; + shacl:path dc:rights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.modificationdate"; + shacl:description "The most recent date on which the Catalogue was modified."@en; + shacl:maxCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.licence"; + shacl:description "A licence under which the Catalogue can be used or reused."@en; + shacl:maxCount 1; + shacl:name "licence"@en; + shacl:path dc:license . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.publisher"; + shacl:description "An entity (organisation) responsible for making the Catalogue available."@en; + shacl:minCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Catalogue."@en; + shacl:maxCount 1; + shacl:name "release date"@en; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.publisher"; + shacl:class foaf:Agent; + shacl:description "An entity (organisation) responsible for making the Catalogue available."@en; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.rights"; + shacl:description "A statement that specifies rights associated with the Catalogue."@en; + shacl:name "rights"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:rights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.catalogue"; + shacl:description "A catalogue whose contents are of interest in the context of this catalogue."@en; + shacl:name "catalogue"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:catalog . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.creator"; + shacl:class foaf:Agent; + shacl:description "An entity responsible for the creation of the catalogue."@en; + shacl:name "creator"@en; + shacl:path dc:creator . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.service"; + shacl:class dcat:DataService; + shacl:description "A site or end-point (Data Service) that is listed in the Catalogue."@en; + shacl:name "service"@en; + shacl:path dcat:service . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.haspart"; + shacl:class dcat:Catalog; + shacl:description "A related Catalogue that is part of the described Catalogue."@en; + shacl:name "has part"@en; + shacl:path dc:hasPart . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.title"; + shacl:description "A name given to the Catalogue."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.homepage"; + shacl:description "A web page that acts as the main page for the Catalogue."@en; + shacl:maxCount 1; + shacl:name "homepage"@en; + shacl:path foaf:homepage . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.language"; + shacl:description "A language used in the textual metadata describing titles, descriptions, etc. of the Datasets in the Catalogue."@en; + shacl:name "language"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.licence"; + shacl:class dc:LicenseDocument; + shacl:description "A licence under which the Catalogue can be used or reused."@en; + shacl:name "licence"@en; + shacl:path dc:license . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.licence"; + shacl:description "A licence under which the Catalogue can be used or reused."@en; + shacl:name "licence"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:license . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.title"; + shacl:description "A name given to the Catalogue."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Catalogue.homepage"; + shacl:class foaf:Document; + shacl:description "A web page that acts as the main page for the Catalogue."@en; + shacl:name "homepage"@en; + shacl:path foaf:homepage . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:DataService . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.conformsto"; + shacl:description "An established (technical) standard to which the Data Service conforms."@en; + shacl:name "conforms to"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.accessrights"; + shacl:description "Information regarding access or restrictions based on privacy, security, or other policies."@en; + shacl:name "access rights"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.servesdataset"; + shacl:class dcat:Dataset; + shacl:description "This property refers to a collection of data that this data service can distribute."@en; + shacl:name "serves dataset"@en; + shacl:path dcat:servesDataset . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.landingpage"; + shacl:description "A web page that provides access to the Data Service and/or additional information."@en; + shacl:name "landing page"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:landingPage . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.keyword"; + shacl:description "A keyword or tag describing the Data Service."@en; + shacl:name "keyword"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:keyword . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.publisher"; + shacl:description "An entity (organisation) responsible for making the Data Service available."@en; + shacl:maxCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.documentation"; + shacl:class foaf:Document; + shacl:description "A page or document about this Data Service"@en; + shacl:name "documentation"@en; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.description"; + shacl:description "A free-text account of the Data Service."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.applicablelegislation"; + shacl:class ; + shacl:description "The legislation that mandates the creation or management of the Data Service."@en; + shacl:name "applicable legislation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.format"; + shacl:class dc:MediaTypeOrExtent; + shacl:description "The structure that can be returned by querying the endpointURL."@en; + shacl:name "format"@en; + shacl:path dc:format . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.accessrights"; + shacl:description "Information regarding access or restrictions based on privacy, security, or other policies."@en; + shacl:maxCount 1; + shacl:name "access rights"@en; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.contactpoint"; + shacl:class vcard:Kind; + shacl:description "Contact information that can be used for sending comments about the Data Service."@en; + shacl:name "contact point"@en; + shacl:path dcat:contactPoint . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.publisher"; + shacl:description "An entity (organisation) responsible for making the Data Service available."@en; + shacl:name "publisher"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.theme"; + shacl:description "A category of the Data Service."@en; + shacl:name "theme"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:theme . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.documentation"; + shacl:description "A page or document about this Data Service"@en; + shacl:name "documentation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.publisher"; + shacl:class foaf:Agent; + shacl:description "An entity (organisation) responsible for making the Data Service available."@en; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.format"; + shacl:description "The structure that can be returned by querying the endpointURL."@en; + shacl:name "format"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:format . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.conformsto"; + shacl:class dc:Standard; + shacl:description "An established (technical) standard to which the Data Service conforms."@en; + shacl:name "conforms to"@en; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.title"; + shacl:description "A name given to the Data Service."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.licence"; + shacl:class dc:LicenseDocument; + shacl:description "A licence under which the Data service is made available."@en; + shacl:name "licence"@en; + shacl:path dc:license . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.endpointdescription"; + shacl:class rdfs:Resource; + shacl:description "A description of the services available via the end-points, including their operations, parameters etc."@en; + shacl:name "endpoint description"@en; + shacl:path dcat:endpointDescription . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.theme"; + shacl:class skos:Concept; + shacl:description "A category of the Data Service."@en; + shacl:name "theme"@en; + shacl:path dcat:theme . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.endpointURL"; + shacl:class rdfs:Resource; + shacl:description "The root location or primary endpoint of the service (an IRI)."@en; + shacl:name "endpoint URL"@en; + shacl:path dcat:endpointURL . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.accessrights"; + shacl:class dc:RightsStatement; + shacl:description "Information regarding access or restrictions based on privacy, security, or other policies."@en; + shacl:name "access rights"@en; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.title"; + shacl:description "A name given to the Data Service."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DataService.landingpage"; + shacl:class foaf:Document; + shacl:description "A web page that provides access to the Data Service and/or additional information."@en; + shacl:name "landing page"@en; + shacl:path dcat:landingPage . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:DatasetSeries . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.geographicalcoverage"; + shacl:class dc:Location; + shacl:description "A geographic region that is covered by the Dataset Series."@en; + shacl:name "geographical coverage"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.frequency"; + shacl:class dc:Frequency; + shacl:description "The frequency at which the Dataset Series is updated."@en; + shacl:name "frequency"@en; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.applicablelegislation"; + shacl:description "The legislation that mandates the creation or management of the Dataset Series."@en; + shacl:name "applicable legislation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Dataset Series."@en; + shacl:name "release date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.frequency"; + shacl:description "The frequency at which the Dataset Series is updated."@en; + shacl:name "frequency"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.publisher"; + shacl:description "An entity (organisation) responsible for ensuring the coherency of the Dataset Series "@en; + shacl:maxCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.modificationdate"; + shacl:description "The most recent date on which the Dataset Series was changed or modified."@en; + shacl:name "modification date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.description"; + shacl:description "A free-text account of the Dataset Series."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.applicablelegislation"; + shacl:class ; + shacl:description "The legislation that mandates the creation or management of the Dataset Series."@en; + shacl:name "applicable legislation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.contactpoint"; + shacl:class vcard:Kind; + shacl:description "Contact information that can be used for sending comments about the Dataset Series."@en; + shacl:name "contact point"@en; + shacl:path dcat:contactPoint . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.temporalcoverage"; + shacl:description "A temporal period that the Dataset Series covers."@en; + shacl:name "temporal coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.frequency"; + shacl:description "The frequency at which the Dataset Series is updated."@en; + shacl:maxCount 1; + shacl:name "frequency"@en; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.temporalcoverage"; + shacl:class dc:PeriodOfTime; + shacl:description "A temporal period that the Dataset Series covers."@en; + shacl:name "temporal coverage"@en; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.description"; + shacl:description "A free-text account of the Dataset Series."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.publisher"; + shacl:description "An entity (organisation) responsible for ensuring the coherency of the Dataset Series "@en; + shacl:name "publisher"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.geographicalcoverage"; + shacl:description "A geographic region that is covered by the Dataset Series."@en; + shacl:name "geographical coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.contactpoint"; + shacl:description "Contact information that can be used for sending comments about the Dataset Series."@en; + shacl:name "contact point"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:contactPoint . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.modificationdate"; + shacl:description "The most recent date on which the Dataset Series was changed or modified."@en; + shacl:maxCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Dataset Series."@en; + shacl:maxCount 1; + shacl:name "release date"@en; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.publisher"; + shacl:class foaf:Agent; + shacl:description "An entity (organisation) responsible for ensuring the coherency of the Dataset Series "@en; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.title"; + shacl:description "A name given to the Dataset Series."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#DatasetSeries.title"; + shacl:description "A name given to the Dataset Series."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:Dataset . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.geographicalcoverage"; + shacl:class dc:Location; + shacl:description "A geographic region that is covered by the Dataset."@en; + shacl:name "geographical coverage"@en; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.otheridentifier"; + shacl:description "A secondary identifier of the Dataset"@en; + shacl:name "other identifier"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.frequency"; + shacl:class dc:Frequency; + shacl:description "The frequency at which the Dataset is updated."@en; + shacl:name "frequency"@en; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.conformsto"; + shacl:description "An implementing rule or other specification."@en; + shacl:name "conforms to"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.relatedresource"; + shacl:description "A related resource."@en; + shacl:name "related resource"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:relation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.provenance"; + shacl:class dc:ProvenanceStatement; + shacl:description "A statement about the lineage of a Dataset."@en; + shacl:name "provenance"@en; + shacl:path dc:provenance . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.accessrights"; + shacl:description "Information that indicates whether the Dataset is publicly accessible, has access restrictions or is not public."@en; + shacl:name "access rights"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.temporalresolution"; + shacl:description "The minimum time period resolvable in the dataset."@en; + shacl:maxCount 1; + shacl:name "temporal resolution"@en; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.version"; + shacl:description "The version indicator (name or identifier) of a resource."@en; + shacl:maxCount 1; + shacl:name "version"@en; + shacl:path dcat:version . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.wasgeneratedby"; + shacl:description "An activity that generated, or provides the business context for, the creation of the dataset."@en; + shacl:name "was generated by"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path prov:wasGeneratedBy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.temporalresolution"; + shacl:description "The minimum time period resolvable in the dataset."@en; + shacl:name "temporal resolution"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.language"; + shacl:class dc:LinguisticSystem; + shacl:description "A language of the Dataset."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.datasetdistribution"; + shacl:class dcat:Distribution; + shacl:description "An available Distribution for the Dataset."@en; + shacl:name "dataset distribution"@en; + shacl:path dcat:distribution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.source"; + shacl:class dcat:Dataset; + shacl:description "A related Dataset from which the described Dataset is derived."@en; + shacl:name "source"@en; + shacl:path dc:source . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.landingpage"; + shacl:description "A web page that provides access to the Dataset, its Distributions and/or additional information."@en; + shacl:name "landing page"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:landingPage . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.type"; + shacl:class skos:Concept; + shacl:description "A type of the Dataset."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.sample"; + shacl:description "A sample distribution of the dataset."@en; + shacl:name "sample"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.wasgeneratedby"; + shacl:class prov:Activity; + shacl:description "An activity that generated, or provides the business context for, the creation of the dataset."@en; + shacl:name "was generated by"@en; + shacl:path prov:wasGeneratedBy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.isreferencedby"; + shacl:description "A related resource, such as a publication, that references, cites, or otherwise points to the dataset."@en; + shacl:name "is referenced by"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:isReferencedBy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.qualifiedrelation"; + shacl:description "A description of a relationship with another resource."@en; + shacl:name "qualified relation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:qualifiedRelation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.spatialresolution"; + shacl:datatype xsd:decimal; + shacl:description "The minimum spatial separation resolvable in a dataset, measured in meters."@en; + shacl:name "spatial resolution"@en; + shacl:path dcat:spatialResolutionInMeters . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.spatialresolution"; + shacl:description "The minimum spatial separation resolvable in a dataset, measured in meters."@en; + shacl:name "spatial resolution"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:spatialResolutionInMeters . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.relatedresource"; + shacl:class rdfs:Resource; + shacl:description "A related resource."@en; + shacl:name "related resource"@en; + shacl:path dc:relation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.hasversion"; + shacl:class dcat:Dataset; + shacl:description "A related Dataset that is a version, edition, or adaptation of the described Dataset."@en; + shacl:name "has version"@en; + shacl:path dcat:hasVersion . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Dataset."@en; + shacl:name "release date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.type"; + shacl:description "A type of the Dataset."@en; + shacl:name "type"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.versionnotes"; + shacl:description "A description of the differences between this version and a previous version of the Dataset."@en; + shacl:name "version notes"@en; + shacl:nodeKind shacl:Literal; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.keyword"; + shacl:description "A keyword or tag describing the Dataset."@en; + shacl:name "keyword"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:keyword . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.provenance"; + shacl:description "A statement about the lineage of a Dataset."@en; + shacl:name "provenance"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:provenance . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.sample"; + shacl:class dcat:Distribution; + shacl:description "A sample distribution of the dataset."@en; + shacl:name "sample"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.inseries"; + shacl:class dcat:DatasetSeries; + shacl:description "A dataset series of which the dataset is part."@en; + shacl:name "in series"@en; + shacl:path dcat:inSeries . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.frequency"; + shacl:description "The frequency at which the Dataset is updated."@en; + shacl:name "frequency"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.publisher"; + shacl:description "An entity (organisation) responsible for making the Dataset available."@en; + shacl:maxCount 1; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.documentation"; + shacl:class foaf:Document; + shacl:description "A page or document about this Dataset."@en; + shacl:name "documentation"@en; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.modificationdate"; + shacl:description "The most recent date on which the Dataset was changed or modified."@en; + shacl:name "modification date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.description"; + shacl:description "A free-text account of the Dataset."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.applicablelegislation"; + shacl:class ; + shacl:description "The legislation that mandates the creation or management of the Dataset."@en; + shacl:name "applicable legislation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.accessrights"; + shacl:description "Information that indicates whether the Dataset is publicly accessible, has access restrictions or is not public."@en; + shacl:maxCount 1; + shacl:name "access rights"@en; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.contactpoint"; + shacl:class vcard:Kind; + shacl:description "Contact information that can be used for sending comments about the Dataset."@en; + shacl:name "contact point"@en; + shacl:path dcat:contactPoint . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.creator"; + shacl:description "An entity responsible for producing the dataset."@en; + shacl:name "creator"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:creator . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.source"; + shacl:description "A related Dataset from which the described Dataset is derived."@en; + shacl:name "source"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:source . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.temporalcoverage"; + shacl:description "A temporal period that the Dataset covers."@en; + shacl:name "temporal coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.inseries"; + shacl:description "A dataset series of which the dataset is part."@en; + shacl:name "in series"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:inSeries . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.qualifiedattribution"; + shacl:class prov:Attribution; + shacl:description "An Agent having some form of responsibility for the resource."@en; + shacl:name "qualified attribution"@en; + shacl:path prov:qualifiedAttribution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.frequency"; + shacl:description "The frequency at which the Dataset is updated."@en; + shacl:maxCount 1; + shacl:name "frequency"@en; + shacl:path dc:accrualPeriodicity . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.temporalcoverage"; + shacl:class dc:PeriodOfTime; + shacl:description "A temporal period that the Dataset covers."@en; + shacl:name "temporal coverage"@en; + shacl:path dc:temporal . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.description"; + shacl:description "A free-text account of the Dataset."@en; + shacl:minCount 1; + shacl:name "description"@en; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.publisher"; + shacl:description "An entity (organisation) responsible for making the Dataset available."@en; + shacl:name "publisher"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.geographicalcoverage"; + shacl:description "A geographic region that is covered by the Dataset."@en; + shacl:name "geographical coverage"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:spatial . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.isreferencedby"; + shacl:class rdfs:Resource; + shacl:description "A related resource, such as a publication, that references, cites, or otherwise points to the dataset."@en; + shacl:name "is referenced by"@en; + shacl:path dc:isReferencedBy . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.theme"; + shacl:description "A category of the Dataset."@en; + shacl:name "theme"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:theme . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.documentation"; + shacl:description "A page or document about this Dataset."@en; + shacl:name "documentation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.modificationdate"; + shacl:description "The most recent date on which the Dataset was changed or modified."@en; + shacl:maxCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.otheridentifier"; + shacl:class ; + shacl:description "A secondary identifier of the Dataset"@en; + shacl:name "other identifier"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.identifier"; + shacl:description "The main identifier for the Dataset, e.g. the URI or other unique identifier in the context of the Catalogue."@en; + shacl:name "identifier"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:identifier . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Dataset."@en; + shacl:maxCount 1; + shacl:name "release date"@en; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.publisher"; + shacl:class foaf:Agent; + shacl:description "An entity (organisation) responsible for making the Dataset available."@en; + shacl:name "publisher"@en; + shacl:path dc:publisher . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.qualifiedattribution"; + shacl:description "An Agent having some form of responsibility for the resource."@en; + shacl:name "qualified attribution"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path prov:qualifiedAttribution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.version"; + shacl:description "The version indicator (name or identifier) of a resource."@en; + shacl:name "version"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:version . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.creator"; + shacl:class foaf:Agent; + shacl:description "An entity responsible for producing the dataset."@en; + shacl:name "creator"@en; + shacl:path dc:creator . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.conformsto"; + shacl:class dc:Standard; + shacl:description "An implementing rule or other specification."@en; + shacl:name "conforms to"@en; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.hasversion"; + shacl:description "A related Dataset that is a version, edition, or adaptation of the described Dataset."@en; + shacl:name "has version"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:hasVersion . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.title"; + shacl:description "A name given to the Dataset."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.language"; + shacl:description "A language of the Dataset."@en; + shacl:name "language"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.theme"; + shacl:class skos:Concept; + shacl:description "A category of the Dataset."@en; + shacl:name "theme"@en; + shacl:path dcat:theme . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.temporalresolution"; + shacl:datatype xsd:duration; + shacl:description "The minimum time period resolvable in the dataset."@en; + shacl:name "temporal resolution"@en; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.accessrights"; + shacl:class dc:RightsStatement; + shacl:description "Information that indicates whether the Dataset is publicly accessible, has access restrictions or is not public."@en; + shacl:name "access rights"@en; + shacl:path dc:accessRights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.datasetdistribution"; + shacl:description "An available Distribution for the Dataset."@en; + shacl:name "dataset distribution"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:distribution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.title"; + shacl:description "A name given to the Dataset."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.qualifiedrelation"; + shacl:class dcat:Relationship; + shacl:description "A description of a relationship with another resource."@en; + shacl:name "qualified relation"@en; + shacl:path dcat:qualifiedRelation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.spatialresolution"; + shacl:description "The minimum spatial separation resolvable in a dataset, measured in meters."@en; + shacl:maxCount 1; + shacl:name "spatial resolution"@en; + shacl:path dcat:spatialResolutionInMeters . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Dataset.landingpage"; + shacl:class foaf:Document; + shacl:description "A web page that provides access to the Dataset, its Distributions and/or additional information."@en; + shacl:name "landing page"@en; + shacl:path dcat:landingPage . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dcat:Distribution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.mediatype"; + shacl:class dc:MediaType; + shacl:description "The media type of the Distribution as defined in the official register of media types managed by IANA."@en; + shacl:name "media type"@en; + shacl:path dcat:mediaType . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.availability"; + shacl:description "An indication how long it is planned to keep the Distribution of the Dataset available."@en; + shacl:name "availability"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.status"; + shacl:class skos:Concept; + shacl:description "The status of the distribution in the context of maturity lifecycle."@en; + shacl:name "status"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.temporalresolution"; + shacl:description "The minimum time period resolvable in the dataset distribution."@en; + shacl:maxCount 1; + shacl:name "temporal resolution"@en; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.compressionformat"; + shacl:description "The format of the file in which the data is contained in a compressed form, e.g. to reduce the size of the downloadable file."@en; + shacl:maxCount 1; + shacl:name "compression format"@en; + shacl:path dcat:compressFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.linkedschemas"; + shacl:class dc:Standard; + shacl:description "An established schema to which the described Distribution conforms."@en; + shacl:name "linked schemas"@en; + shacl:path dc:conformsTo . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.downloadURL"; + shacl:description "A URL that is a direct link to a downloadable file in a given format."@en; + shacl:name "download URL"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:downloadURL . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.temporalresolution"; + shacl:description "The minimum time period resolvable in the dataset distribution."@en; + shacl:name "temporal resolution"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.language"; + shacl:class dc:LinguisticSystem; + shacl:description "A language used in the Distribution."@en; + shacl:name "language"@en; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.status"; + shacl:description "The status of the distribution in the context of maturity lifecycle."@en; + shacl:maxCount 1; + shacl:name "status"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.haspolicy"; + shacl:description "The policy expressing the rights associated with the distribution if using the [[ODRL]] vocabulary."@en; + shacl:maxCount 1; + shacl:name "has policy"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.rights"; + shacl:description "A statement that specifies rights associated with the Distribution."@en; + shacl:maxCount 1; + shacl:name "rights"@en; + shacl:path dc:rights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.packagingformat"; + shacl:class dc:MediaType; + shacl:description "The format of the file in which one or more data files are grouped together, e.g. to enable a set of related files to be downloaded together."@en; + shacl:name "packaging format"@en; + shacl:path dcat:packageFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.status"; + shacl:description "The status of the distribution in the context of maturity lifecycle."@en; + shacl:name "status"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.spatialresolution"; + shacl:datatype xsd:decimal; + shacl:description "The minimum spatial separation resolvable in a dataset distribution, measured in meters."@en; + shacl:name "spatial resolution"@en; + shacl:path dcat:spatialResolutionInMeters . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.mediatype"; + shacl:description "The media type of the Distribution as defined in the official register of media types managed by IANA."@en; + shacl:name "media type"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:mediaType . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.spatialresolution"; + shacl:description "The minimum spatial separation resolvable in a dataset distribution, measured in meters."@en; + shacl:name "spatial resolution"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:spatialResolutionInMeters . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Distribution."@en; + shacl:name "release date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.bytesize"; + shacl:description "The size of a Distribution in bytes."@en; + shacl:name "byte size"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:byteSize . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.checksum"; + shacl:class ; + shacl:description "A mechanism that can be used to verify that the contents of a distribution have not changed."@en; + shacl:name "checksum"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.accessURL"; + shacl:class rdfs:Resource; + shacl:description "A URL that gives access to a Distribution of the Dataset."@en; + shacl:name "access URL"@en; + shacl:path dcat:accessURL . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.documentation"; + shacl:class foaf:Document; + shacl:description "A page or document about this Distribution."@en; + shacl:name "documentation"@en; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.mediatype"; + shacl:description "The media type of the Distribution as defined in the official register of media types managed by IANA."@en; + shacl:maxCount 1; + shacl:name "media type"@en; + shacl:path dcat:mediaType . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.modificationdate"; + shacl:description "The most recent date on which the Distribution was changed or modified."@en; + shacl:name "modification date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.description"; + shacl:description "A free-text account of the Distribution."@en; + shacl:name "description"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:description . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.applicablelegislation"; + shacl:class ; + shacl:description "The legislation that mandates the creation or management of the Distribution."@en; + shacl:name "applicable legislation"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.checksum"; + shacl:description "A mechanism that can be used to verify that the contents of a distribution have not changed."@en; + shacl:maxCount 1; + shacl:name "checksum"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.format"; + shacl:class dc:MediaTypeOrExtent; + shacl:description "The file format of the Distribution."@en; + shacl:name "format"@en; + shacl:path dc:format . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.accessservice"; + shacl:class dcat:DataService; + shacl:description "A data service that gives access to the distribution of the dataset."@en; + shacl:name "access service"@en; + shacl:path dcat:accessService . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.compressionformat"; + shacl:description "The format of the file in which the data is contained in a compressed form, e.g. to reduce the size of the downloadable file."@en; + shacl:name "compression format"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:compressFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.haspolicy"; + shacl:class ; + shacl:description "The policy expressing the rights associated with the distribution if using the [[ODRL]] vocabulary."@en; + shacl:name "has policy"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.format"; + shacl:description "The file format of the Distribution."@en; + shacl:maxCount 1; + shacl:name "format"@en; + shacl:path dc:format . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.rights"; + shacl:class dc:RightsStatement; + shacl:description "A statement that specifies rights associated with the Distribution."@en; + shacl:name "rights"@en; + shacl:path dc:rights . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.documentation"; + shacl:description "A page or document about this Distribution."@en; + shacl:name "documentation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path foaf:page . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.availability"; + shacl:class skos:Concept; + shacl:description "An indication how long it is planned to keep the Distribution of the Dataset available."@en; + shacl:name "availability"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.modificationdate"; + shacl:description "The most recent date on which the Distribution was changed or modified."@en; + shacl:maxCount 1; + shacl:name "modification date"@en; + shacl:path dc:modified . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.downloadURL"; + shacl:class rdfs:Resource; + shacl:description "A URL that is a direct link to a downloadable file in a given format."@en; + shacl:name "download URL"@en; + shacl:path dcat:downloadURL . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.availability"; + shacl:description "An indication how long it is planned to keep the Distribution of the Dataset available."@en; + shacl:maxCount 1; + shacl:name "availability"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.releasedate"; + shacl:description "The date of formal issuance (e.g., publication) of the Distribution."@en; + shacl:maxCount 1; + shacl:name "release date"@en; + shacl:path dc:issued . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.packagingformat"; + shacl:description "The format of the file in which one or more data files are grouped together, e.g. to enable a set of related files to be downloaded together."@en; + shacl:maxCount 1; + shacl:name "packaging format"@en; + shacl:path dcat:packageFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.format"; + shacl:description "The file format of the Distribution."@en; + shacl:name "format"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:format . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.packagingformat"; + shacl:description "The format of the file in which one or more data files are grouped together, e.g. to enable a set of related files to be downloaded together."@en; + shacl:name "packaging format"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:packageFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.title"; + shacl:description "A name given to the Distribution."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.language"; + shacl:description "A language used in the Distribution."@en; + shacl:name "language"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:language . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.licence"; + shacl:class dc:LicenseDocument; + shacl:description "A licence under which the Distribution is made available."@en; + shacl:name "licence"@en; + shacl:path dc:license . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.checksum"; + shacl:description "A mechanism that can be used to verify that the contents of a distribution have not changed."@en; + shacl:name "checksum"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.compressionformat"; + shacl:class dc:MediaType; + shacl:description "The format of the file in which the data is contained in a compressed form, e.g. to reduce the size of the downloadable file."@en; + shacl:name "compression format"@en; + shacl:path dcat:compressFormat . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.temporalresolution"; + shacl:datatype xsd:duration; + shacl:description "The minimum time period resolvable in the dataset distribution."@en; + shacl:name "temporal resolution"@en; + shacl:path dcat:temporalResolution . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.bytesize"; + shacl:datatype xsd:nonNegativeInteger; + shacl:description "The size of a Distribution in bytes."@en; + shacl:name "byte size"@en; + shacl:path dcat:byteSize . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#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 . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.bytesize"; + shacl:description "The size of a Distribution in bytes."@en; + shacl:maxCount 1; + shacl:name "byte size"@en; + shacl:path dcat:byteSize . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.haspolicy"; + shacl:description "The policy expressing the rights associated with the distribution if using the [[ODRL]] vocabulary."@en; + shacl:name "has policy"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Distribution.spatialresolution"; + shacl:description "The minimum spatial separation resolvable in a dataset distribution, measured in meters."@en; + shacl:maxCount 1; + shacl:name "spatial resolution"@en; + shacl:path dcat:spatialResolutionInMeters . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + ; + shacl:targetClass dcat:Relationship . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.relation"; + shacl:description "A resource related to the source resource."@en; + shacl:name "relation"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:relation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.hadrole"; + shacl:description "A function of an entity or agent with respect to another entity or resource."@en; + shacl:name "had role"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dcat:hadRole . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.relation"; + shacl:description "A resource related to the source resource."@en; + shacl:minCount 1; + shacl:name "relation"@en; + shacl:path dc:relation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.hadrole"; + shacl:class dcat:Role; + shacl:description "A function of an entity or agent with respect to another entity or resource."@en; + shacl:name "had role"@en; + shacl:path dcat:hadRole . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.relation"; + shacl:class rdfs:Resource; + shacl:description "A resource related to the source resource."@en; + shacl:name "relation"@en; + shacl:path dc:relation . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Relationship.hadrole"; + shacl:description "A function of an entity or agent with respect to another entity or resource."@en; + shacl:minCount 1; + shacl:name "had role"@en; + shacl:path dcat:hadRole . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dcat:Resource . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dcat:Role . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:Frequency . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + ; + shacl:targetClass dc:LicenseDocument . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#LicenceDocument.type"; + shacl:class skos:Concept; + shacl:description "A type of licence, e.g. indicating 'public domain' or 'royalties required'."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#LicenceDocument.type"; + shacl:description "A type of licence, e.g. indicating 'public domain' or 'royalties required'."@en; + shacl:name "type"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:type . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:LinguisticSystem . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + ; + shacl:targetClass dc:Location . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.bbox"; + shacl:description "The geographic bounding box of a resource."@en; + shacl:name "bbox"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:bbox . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.geometry"; + shacl:class ; + shacl:description "The corresponding geometry for a resource."@en; + shacl:name "geometry"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.centroid"; + shacl:description "The geographic center (centroid) of a resource."@en; + shacl:name "centroid"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:centroid . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.centroid"; + shacl:description "The geographic center (centroid) of a resource."@en; + shacl:maxCount 1; + shacl:name "centroid"@en; + shacl:path dcat:centroid . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.geometry"; + shacl:description "The corresponding geometry for a resource."@en; + shacl:name "geometry"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.bbox"; + shacl:description "The geographic bounding box of a resource."@en; + shacl:maxCount 1; + shacl:name "bbox"@en; + shacl:path dcat:bbox . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Location.geometry"; + shacl:description "The corresponding geometry for a resource."@en; + shacl:maxCount 1; + shacl:name "geometry"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:MediaTypeOrExtent . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:MediaType . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + , + , + ; + shacl:targetClass dc:PeriodOfTime . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.beginning"; + shacl:description "The beginning of a period or interval."@en; + shacl:maxCount 1; + shacl:name "beginning"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.beginning"; + shacl:description "The beginning of a period or interval."@en; + shacl:name "beginning"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.end"; + shacl:description "The end of a period or interval."@en; + shacl:maxCount 1; + shacl:name "end"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.startdate"; + shacl:description "The start of the period."@en; + shacl:name "start date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:startDate . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.enddate"; + shacl:description "The end of the period."@en; + shacl:maxCount 1; + shacl:name "end date"@en; + shacl:path dcat:endDate . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.end"; + shacl:class ; + shacl:description "The end of a period or interval."@en; + shacl:name "end"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.end"; + shacl:description "The end of a period or interval."@en; + shacl:name "end"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.enddate"; + shacl:description "The end of the period."@en; + shacl:name "end date"@en; + shacl:nodeKind shacl:Literal; + shacl:path dcat:endDate . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.beginning"; + shacl:class ; + shacl:description "The beginning of a period or interval."@en; + shacl:name "beginning"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Periodoftime.startdate"; + shacl:description "The start of the period."@en; + shacl:maxCount 1; + shacl:name "start date"@en; + shacl:path dcat:startDate . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:ProvenanceStatement . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:RightsStatement . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:Standard . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass dc:MediaType . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + ; + shacl:targetClass foaf:Agent . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Agent.type"; + shacl:description "The nature of the agent."@en; + shacl:maxCount 1; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Agent.type"; + shacl:class skos:Concept; + shacl:description "The nature of the agent."@en; + shacl:name "type"@en; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Agent.type"; + shacl:description "The nature of the agent."@en; + shacl:name "type"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path dc:type . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Agent.name"; + shacl:description "A name of the agent."@en; + shacl:minCount 1; + shacl:name "name"@en; + shacl:path foaf:name . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Agent.name"; + shacl:description "A name of the agent."@en; + shacl:name "name"@en; + shacl:nodeKind shacl:Literal; + shacl:path foaf:name . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass foaf:Document . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass prov:Activity . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass prov:Attribution . + + 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:property , + ; + shacl:targetClass skos:ConceptScheme . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#ConceptScheme.title"; + shacl:description "A name of the concept scheme."@en; + shacl:name "title"@en; + shacl:nodeKind shacl:Literal; + shacl:path dc:title . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#ConceptScheme.title"; + shacl:description "A name of the concept scheme."@en; + shacl:minCount 1; + shacl:name "title"@en; + shacl:path dc:title . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + ; + shacl:targetClass skos:Concept . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Concept.preferredlabel"; + shacl:description "A preferred label of the concept."@en; + shacl:name "preferred label"@en; + shacl:nodeKind shacl:Literal; + shacl:path skos:prefLabel . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Concept.preferredlabel"; + shacl:description "A preferred label of the concept."@en; + shacl:minCount 1; + shacl:name "preferred label"@en; + shacl:path skos:prefLabel . + + a shacl:NodeShape; + shacl:closed false; + shacl:property , + , + , + , + , + , + , + ; + shacl:targetClass . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.checksumvalue"; + shacl:description "A lower case hexadecimal encoded digest value produced using a specific algorithm."@en; + shacl:name "checksum value"@en; + shacl:nodeKind shacl:Literal; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.checksumvalue"; + shacl:datatype xsd:hexBinary; + shacl:description "A lower case hexadecimal encoded digest value produced using a specific algorithm."@en; + shacl:name "checksum value"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.checksumvalue"; + shacl:description "A lower case hexadecimal encoded digest value produced using a specific algorithm."@en; + shacl:maxCount 1; + shacl:name "checksum value"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.algorithm"; + shacl:description "The algorithm used to produce the subject Checksum."@en; + shacl:name "algorithm"@en; + shacl:nodeKind shacl:BlankNodeOrIRI; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.algorithm"; + shacl:description "The algorithm used to produce the subject Checksum."@en; + shacl:maxCount 1; + shacl:name "algorithm"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.algorithm"; + shacl:description "The algorithm used to produce the subject Checksum."@en; + shacl:minCount 1; + shacl:name "algorithm"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.checksumvalue"; + shacl:description "A lower case hexadecimal encoded digest value produced using a specific algorithm."@en; + shacl:minCount 1; + shacl:name "checksum value"@en; + shacl:path . + + rdfs:seeAlso "https://semiceu.github.io/DCAT-AP/releases/3.0.0#Checksum.algorithm"; + shacl:class ; + shacl:description "The algorithm used to produce the subject Checksum."@en; + shacl:name "algorithm"@en; + shacl:path . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass vcard:Kind . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:dateTime . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:decimal . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:duration . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:hexBinary . + + a shacl:NodeShape; + shacl:closed false; + shacl:targetClass xsd:nonNegativeInteger . diff --git a/ckanext/dcat/tests/test_shacl.py b/ckanext/dcat/tests/test_shacl.py index ac293b37..ceb4f626 100644 --- a/ckanext/dcat/tests/test_shacl.py +++ b/ckanext/dcat/tests/test_shacl.py @@ -162,3 +162,34 @@ def test_validate_dcat_ap_2_graph_shapes_range(): ] assert set(failures) - set(known_failures) == set(), results_text + + +@pytest.mark.usefixtures("with_plugins", "clean_db") +@pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") +@pytest.mark.ckan_config( + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" +) +@pytest.mark.ckan_config( + "scheming.presets", + "ckanext.scheming:presets.json ckanext.dcat.schemas:presets.yaml", +) +@pytest.mark.ckan_config( + "ckanext.dcat.rdf.profiles", "euro_dcat_ap_3" +) +def test_validate_dcat_ap_3_graph(): + + graph = graph_from_dataset("ckan_full_dataset_dcat_ap_2_vocabularies.json") + graph.serialize(destination="graph.ttl") + path = _get_shacl_file_path("dcat-ap_3_shacl_shapes.ttl") + r = validate(graph, shacl_graph=path) + conforms, results_graph, results_text = r + with open("shacl_results.txt", "w") as f: + f.write(results_text) + + + assert conforms, results_text + + with open("shacl_results.txt", "w") as f: + f.write(results_text) + + diff --git a/setup.py b/setup.py index 78fb19fa..1ecfdfbd 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ [ckan.rdf.profiles] euro_dcat_ap=ckanext.dcat.profiles:EuropeanDCATAPProfile euro_dcat_ap_2=ckanext.dcat.profiles:EuropeanDCATAP2Profile + euro_dcat_ap_3=ckanext.dcat.profiles:EuropeanDCATAP3Profile euro_dcat_ap_scheming=ckanext.dcat.profiles:EuropeanDCATAPSchemingProfile schemaorg=ckanext.dcat.profiles:SchemaOrgProfile From 00603a4b42b4ea77be2e172c5f7e91ca0bd9af0e Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 22 Aug 2024 11:15:55 +0200 Subject: [PATCH 02/14] Add RDFS.Resource class to endpoint description --- ckanext/dcat/profiles/euro_dcat_ap_2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_2.py b/ckanext/dcat/profiles/euro_dcat_ap_2.py index efaf8d50..b9e07775 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_2.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_2.py @@ -333,6 +333,7 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): DCAT.endpointDescription, None, URIRefOrLiteral, + RDFS.Resource, ), ("description", DCT.description, None, Literal), ] From c8f708a22f0db1d703e2567abf195660faf484ab Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 22 Aug 2024 12:16:47 +0200 Subject: [PATCH 03/14] Add SPDX.ChecksumAlgorithm if value is URI --- ckanext/dcat/profiles/euro_dcat_ap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ckanext/dcat/profiles/euro_dcat_ap.py b/ckanext/dcat/profiles/euro_dcat_ap.py index 7629cd41..7d71f372 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap.py +++ b/ckanext/dcat/profiles/euro_dcat_ap.py @@ -580,13 +580,16 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): ) if resource_dict.get("hash_algorithm"): + checksum_algo = URIRefOrLiteral(resource_dict["hash_algorithm"]) g.add( ( checksum, SPDX.algorithm, - URIRefOrLiteral(resource_dict["hash_algorithm"]), + checksum_algo, ) ) + if isinstance(checksum_algo, URIRef): + g.add((checksum_algo, RDF.type, SPDX.ChecksumAlgorithm)) g.add((distribution, SPDX.checksum, checksum)) From d4803138163e88949f46d6bbecc3b7d00436b721 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 22 Aug 2024 13:16:52 +0200 Subject: [PATCH 04/14] Distribution byte size should be nonNegativeInteger --- ckanext/dcat/profiles/euro_dcat_ap_3.py | 21 +++++++++++- .../test_euro_dcatap_3_profile_serialize.py | 34 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py diff --git a/ckanext/dcat/profiles/euro_dcat_ap_3.py b/ckanext/dcat/profiles/euro_dcat_ap_3.py index e4fbd1a6..44f68c75 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_3.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_3.py @@ -1,4 +1,6 @@ -from ckanext.dcat.profiles import EuropeanDCATAP2Profile +from rdflib import Literal + +from ckanext.dcat.profiles import EuropeanDCATAP2Profile, DCAT, XSD class EuropeanDCATAP3Profile(EuropeanDCATAP2Profile): @@ -6,3 +8,20 @@ class EuropeanDCATAP3Profile(EuropeanDCATAP2Profile): An RDF profile based on the DCAT-AP 3 for data portals in Europe """ + def graph_from_dataset(self, dataset_dict, dataset_ref): + + # Call the DCAT AP 2 method + super().graph_from_dataset(dataset_dict, dataset_ref) + + # byteSize decimal -> nonNegativeInteger + for subject, predicate, object in self.g.triples((None, DCAT.byteSize, None)): + if object and object.datatype == XSD.decimal: + self.g.remove((subject, predicate, object)) + + self.g.add( + ( + subject, + predicate, + Literal(int(object), datatype=XSD.nonNegativeInteger), + ) + ) diff --git a/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py b/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py new file mode 100644 index 00000000..62e58928 --- /dev/null +++ b/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py @@ -0,0 +1,34 @@ +from ckanext.dcat.profiles import DCAT, XSD +from ckanext.dcat.processors import RDFSerializer +from ckanext.dcat.tests.utils import BaseSerializeTest + +DCAT_AP_PROFILES = ["euro_dcat_ap_3"] + + +class TestEuroDCATAP2ProfileSerializeDataset(BaseSerializeTest): + + def test_byte_size_non_negative_integer(self): + + dataset = { + "id": "4b6fe9ca-dc77-4cec-92a4-55c6624a5bd6", + "name": "test-dataset", + "title": "Test DCAT 2 dataset", + "notes": "Lorem ipsum", + "resources": [ + { + "id": "7fffe9b2-7a24-4d43-91f7-8bd58bad9615", + "url": "http://example.org/data.csv", + "size": 1234, + } + ], + } + + s = RDFSerializer(profiles=DCAT_AP_PROFILES) + g = s.g + + dataset_ref = s.graph_from_dataset(dataset) + + triple = [t for t in g.triples((None, DCAT.byteSize, None))][0] + + assert triple[2].datatype == XSD.nonNegativeInteger + assert int(triple[2]) == 1234 From c0b5007cb02974729cd21853e51d4064e22737d6 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 22 Aug 2024 15:33:04 +0200 Subject: [PATCH 05/14] Fix class bug in serializer --- ckanext/dcat/profiles/euro_dcat_ap_2.py | 1 + examples/ckan/ckan_full_dataset_dcat_ap_2_vocabularies.json | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_2.py b/ckanext/dcat/profiles/euro_dcat_ap_2.py index b9e07775..b5683b1f 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_2.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_2.py @@ -204,6 +204,7 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): fallbacks=fallbacks, _type=type, _datatype=datatype, + _class=_class, ) # Temporal diff --git a/examples/ckan/ckan_full_dataset_dcat_ap_2_vocabularies.json b/examples/ckan/ckan_full_dataset_dcat_ap_2_vocabularies.json index 1dc67e89..4ce21a16 100644 --- a/examples/ckan/ckan_full_dataset_dcat_ap_2_vocabularies.json +++ b/examples/ckan/ckan_full_dataset_dcat_ap_2_vocabularies.json @@ -42,8 +42,7 @@ "https://joinup.ec.europa.eu/collection/semic-support-centre/solution/dcat-application-profile-data-portals-europe/release/210" ], "is_referenced_by": [ - "https://doi.org/10.1038/sdata.2018.22", - "test_isreferencedby" + "https://doi.org/10.1038/sdata.2018.22" ], "applicable_legislation": [ "http://data.europa.eu/eli/reg_impl/2023/138/oj", From 25ae659a64dbcc4d89e5f103a8ae484aaa12b13d Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:27:31 +0200 Subject: [PATCH 06/14] PackageExtra no longer available on CKAN>2.11 Need to use Package.extras instead https://github.com/ckan/ckan/pull/8288 --- ckanext/dcat/harvesters/base.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ckanext/dcat/harvesters/base.py b/ckanext/dcat/harvesters/base.py index a248ef10..64412456 100644 --- a/ckanext/dcat/harvesters/base.py +++ b/ckanext/dcat/harvesters/base.py @@ -159,13 +159,18 @@ def _read_datasets_from_db(self, guid): ''' Returns a database result of datasets matching the given guid. ''' + if toolkit.check_ckan_version(max_version="2.11"): + datasets = model.Session.query(model.Package.id) \ + .join(model.PackageExtra) \ + .filter(model.PackageExtra.key == 'guid') \ + .filter(model.PackageExtra.value == guid) \ + .filter(model.Package.state == 'active') \ + .all() + else: + datasets = model.Session.query(model.Package.id) \ + .filter(model.Package.extras['guid'] == f'"{guid}"') \ + .all() - datasets = model.Session.query(model.Package.id) \ - .join(model.PackageExtra) \ - .filter(model.PackageExtra.key == 'guid') \ - .filter(model.PackageExtra.value == guid) \ - .filter(model.Package.state == 'active') \ - .all() return datasets def _get_existing_dataset(self, guid): From b8d89cdf8616f60aa6642d9209b6f9a3780c98bb Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:33:56 +0200 Subject: [PATCH 07/14] Refactor profiles to make them easier to extend Move from an inheritance approach to a more "composition" one, separating properties handling in three kinds of methods: * `_base()` methods with common properties to *all* versions * `_vX()` methods with properties also applied to higher versions * `_vX_only()` methods with properties only applied to the current version So for instance, the DCAT AP 3 profile calls the following methods in order: ``` def graph_from_dataset(self, dataset_dict, dataset_ref): # Call base method for common properties self._graph_from_dataset_base(dataset_dict, dataset_ref) # DCAT AP v2 properties also applied to higher versions self._graph_from_dataset_v2(dataset_dict, dataset_ref) # DCAT AP v2 scheming fields self._graph_from_dataset_v2_scheming(dataset_dict, dataset_ref) # DCAT AP v3 properties also applied to higher versions self._graph_from_dataset_v3(dataset_dict, dataset_ref) ``` --- ckanext/dcat/profiles/euro_dcat_ap.py | 641 +----------------- ckanext/dcat/profiles/euro_dcat_ap_2.py | 79 ++- ckanext/dcat/profiles/euro_dcat_ap_3.py | 57 +- ckanext/dcat/profiles/euro_dcat_ap_base.py | 629 +++++++++++++++++ .../dcat/profiles/euro_dcat_ap_scheming.py | 22 +- .../test_euro_dcatap_profile_serialize.py | 2 +- ckanext/dcat/tests/test_shacl.py | 20 +- 7 files changed, 798 insertions(+), 652 deletions(-) create mode 100644 ckanext/dcat/profiles/euro_dcat_ap_base.py diff --git a/ckanext/dcat/profiles/euro_dcat_ap.py b/ckanext/dcat/profiles/euro_dcat_ap.py index 7d71f372..c2ac56b9 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap.py +++ b/ckanext/dcat/profiles/euro_dcat_ap.py @@ -1,46 +1,10 @@ -import json -from decimal import Decimal, DecimalException +from .base import ADMS, URIRefOrLiteral +from .euro_dcat_ap_base import BaseEuropeanDCATAPProfile -from rdflib import term, URIRef, BNode, Literal -import ckantoolkit as toolkit -from ckan.lib.munge import munge_tag - -from ckanext.dcat.utils import ( - resource_uri, - DCAT_EXPOSE_SUBCATALOGS, - DCAT_CLEAN_TAGS, - publisher_uri_organization_fallback, -) -from .base import RDFProfile, URIRefOrLiteral, CleanedURIRef -from .base import ( - RDF, - XSD, - SKOS, - RDFS, - DCAT, - DCT, - ADMS, - VCARD, - FOAF, - SCHEMA, - LOCN, - GSP, - OWL, - SPDX, - GEOJSON_IMT, - namespaces, -) - -config = toolkit.config - - -DISTRIBUTION_LICENSE_FALLBACK_CONFIG = "ckanext.dcat.resource.inherit.license" - - -class EuropeanDCATAPProfile(RDFProfile): +class EuropeanDCATAPProfile(BaseEuropeanDCATAPProfile): """ - An RDF profile based on the DCAT-AP for data portals in Europe + An RDF profile based on the DCAT-AP v1 for data portals in Europe More information and specification: @@ -50,585 +14,36 @@ class EuropeanDCATAPProfile(RDFProfile): def parse_dataset(self, dataset_dict, dataset_ref): - dataset_dict["extras"] = [] - dataset_dict["resources"] = [] - - # Basic fields - for key, predicate in ( - ("title", DCT.title), - ("notes", DCT.description), - ("url", DCAT.landingPage), - ("version", OWL.versionInfo), - ): - value = self._object_value(dataset_ref, predicate) - if value: - dataset_dict[key] = value - - if not dataset_dict.get("version"): - # adms:version was supported on the first version of the DCAT-AP - value = self._object_value(dataset_ref, ADMS.version) - if value: - dataset_dict["version"] = value - - # Tags - # replace munge_tag to noop if there's no need to clean tags - do_clean = toolkit.asbool(config.get(DCAT_CLEAN_TAGS, False)) - tags_val = [ - munge_tag(tag) if do_clean else tag for tag in self._keywords(dataset_ref) - ] - tags = [{"name": tag} for tag in tags_val] - dataset_dict["tags"] = tags - - # Extras - - # Simple values - for key, predicate in ( - ("issued", DCT.issued), - ("modified", DCT.modified), - ("identifier", DCT.identifier), - ("version_notes", ADMS.versionNotes), - ("frequency", DCT.accrualPeriodicity), - ("provenance", DCT.provenance), - ("dcat_type", DCT.type), - ): - value = self._object_value(dataset_ref, predicate) - if value: - dataset_dict["extras"].append({"key": key, "value": value}) - - # Lists - for key, predicate, in ( - ("language", DCT.language), - ("theme", DCAT.theme), - ("alternate_identifier", ADMS.identifier), - ("conforms_to", DCT.conformsTo), - ("documentation", FOAF.page), - ("related_resource", DCT.relation), - ("has_version", DCT.hasVersion), - ("is_version_of", DCT.isVersionOf), - ("source", DCT.source), - ("sample", ADMS.sample), - ): - values = self._object_value_list(dataset_ref, predicate) - if values: - dataset_dict["extras"].append({"key": key, "value": json.dumps(values)}) - - # Contact details - contact = self._contact_details(dataset_ref, DCAT.contactPoint) - if not contact: - # adms:contactPoint was supported on the first version of DCAT-AP - contact = self._contact_details(dataset_ref, ADMS.contactPoint) - - if contact: - for key in ("uri", "name", "email"): - if contact.get(key): - dataset_dict["extras"].append( - {"key": "contact_{0}".format(key), "value": contact.get(key)} - ) - - # Publisher - publisher = self._publisher(dataset_ref, DCT.publisher) - for key in ("uri", "name", "email", "url", "type"): - if publisher.get(key): - dataset_dict["extras"].append( - {"key": "publisher_{0}".format(key), "value": publisher.get(key)} - ) - - # Temporal - start, end = self._time_interval(dataset_ref, DCT.temporal) - if start: - dataset_dict["extras"].append({"key": "temporal_start", "value": start}) - if end: - dataset_dict["extras"].append({"key": "temporal_end", "value": end}) - - # Spatial - spatial = self._spatial(dataset_ref, DCT.spatial) - for key in ("uri", "text", "geom"): - self._add_spatial_to_dict(dataset_dict, key, spatial) - - # Dataset URI (explicitly show the missing ones) - dataset_uri = str(dataset_ref) if isinstance(dataset_ref, term.URIRef) else "" - dataset_dict["extras"].append({"key": "uri", "value": dataset_uri}) - - # access_rights - access_rights = self._access_rights(dataset_ref, DCT.accessRights) - if access_rights: - dataset_dict["extras"].append( - {"key": "access_rights", "value": access_rights} - ) - - # License - if "license_id" not in dataset_dict: - dataset_dict["license_id"] = self._license(dataset_ref) - - # Source Catalog - if toolkit.asbool(config.get(DCAT_EXPOSE_SUBCATALOGS, False)): - catalog_src = self._get_source_catalog(dataset_ref) - if catalog_src is not None: - src_data = self._extract_catalog_dict(catalog_src) - dataset_dict["extras"].extend(src_data) - - # Resources - for distribution in self._distributions(dataset_ref): - - resource_dict = {} - - # Simple values - for key, predicate in ( - ("name", DCT.title), - ("description", DCT.description), - ("access_url", DCAT.accessURL), - ("download_url", DCAT.downloadURL), - ("issued", DCT.issued), - ("modified", DCT.modified), - ("status", ADMS.status), - ("license", DCT.license), - ): - value = self._object_value(distribution, predicate) - if value: - resource_dict[key] = value - - resource_dict["url"] = self._object_value( - distribution, DCAT.downloadURL - ) or self._object_value(distribution, DCAT.accessURL) - # Lists - for key, predicate in ( - ("language", DCT.language), - ("documentation", FOAF.page), - ("conforms_to", DCT.conformsTo), - ): - values = self._object_value_list(distribution, predicate) - if values: - resource_dict[key] = json.dumps(values) - - # rights - rights = self._access_rights(distribution, DCT.rights) - if rights: - resource_dict["rights"] = rights - - # Format and media type - normalize_ckan_format = toolkit.asbool( - config.get("ckanext.dcat.normalize_ckan_format", True) - ) - imt, label = self._distribution_format(distribution, normalize_ckan_format) - - if imt: - resource_dict["mimetype"] = imt - - if label: - resource_dict["format"] = label - elif imt: - resource_dict["format"] = imt - - # Size - size = self._object_value_int(distribution, DCAT.byteSize) - if size is not None: - resource_dict["size"] = size - - # Checksum - for checksum in self.g.objects(distribution, SPDX.checksum): - algorithm = self._object_value(checksum, SPDX.algorithm) - checksum_value = self._object_value(checksum, SPDX.checksumValue) - if algorithm: - resource_dict["hash_algorithm"] = algorithm - if checksum_value: - resource_dict["hash"] = checksum_value - - # Distribution URI (explicitly show the missing ones) - resource_dict["uri"] = ( - str(distribution) if isinstance(distribution, term.URIRef) else "" - ) - - # Remember the (internal) distribution reference for referencing in - # further profiles, e.g. for adding more properties - resource_dict["distribution_ref"] = str(distribution) - - dataset_dict["resources"].append(resource_dict) - - if self.compatibility_mode: - # Tweak the resulting dict to make it compatible with previous - # versions of the ckanext-dcat parsers - for extra in dataset_dict["extras"]: - if extra["key"] in ( - "issued", - "modified", - "publisher_name", - "publisher_email", - ): - - extra["key"] = "dcat_" + extra["key"] - - if extra["key"] == "language": - extra["value"] = ",".join(sorted(json.loads(extra["value"]))) + # Call base method for common properties + dataset_dict = self._parse_dataset_base(dataset_dict, dataset_ref) return dataset_dict def graph_from_dataset(self, dataset_dict, dataset_ref): - g = self.g - - for prefix, namespace in namespaces.items(): - g.bind(prefix, namespace) - - g.add((dataset_ref, RDF.type, DCAT.Dataset)) + # Call base method for common properties + self._graph_from_dataset_base(dataset_dict, dataset_ref) - # Basic fields - items = [ - ("title", DCT.title, None, Literal), - ("notes", DCT.description, None, Literal), - ("url", DCAT.landingPage, None, URIRef, FOAF.Document), - ("identifier", DCT.identifier, ["guid", "id"], URIRefOrLiteral), - ("version", OWL.versionInfo, ["dcat_version"], Literal), - ("version_notes", ADMS.versionNotes, None, Literal), - ("frequency", DCT.accrualPeriodicity, None, URIRefOrLiteral, DCT.Frequency), - ("access_rights", DCT.accessRights, None, URIRefOrLiteral, DCT.AccessRights), - ("dcat_type", DCT.type, None, URIRefOrLiteral), - ("provenance", DCT.provenance, None, URIRefOrLiteral, DCT.ProvenanceStatement), - ] - self._add_triples_from_dict(dataset_dict, dataset_ref, items) - - # Tags - for tag in dataset_dict.get("tags", []): - g.add((dataset_ref, DCAT.keyword, Literal(tag["name"]))) - - # Dates - items = [ - ("issued", DCT.issued, ["metadata_created"], Literal), - ("modified", DCT.modified, ["metadata_modified"], Literal), - ] - self._add_date_triples_from_dict(dataset_dict, dataset_ref, items) - - # Lists - items = [ - ("language", DCT.language, None, URIRefOrLiteral, DCT.LinguisticSystem), - ("theme", DCAT.theme, None, URIRef), - ("conforms_to", DCT.conformsTo, None, URIRefOrLiteral, DCT.Standard), - ("alternate_identifier", ADMS.identifier, None, URIRefOrLiteral, ADMS.Identifier), - ("documentation", FOAF.page, None, URIRefOrLiteral, FOAF.Document), - ("related_resource", DCT.relation, None, URIRefOrLiteral, RDFS.Resource), - ("has_version", DCT.hasVersion, None, URIRefOrLiteral), - ("is_version_of", DCT.isVersionOf, None, URIRefOrLiteral), - ("source", DCT.source, None, URIRefOrLiteral), - ("sample", ADMS.sample, None, URIRefOrLiteral, DCAT.Distribution), - ] - self._add_list_triples_from_dict(dataset_dict, dataset_ref, items) - - # Contact details - if any( - [ - self._get_dataset_value(dataset_dict, "contact_uri"), - self._get_dataset_value(dataset_dict, "contact_name"), - self._get_dataset_value(dataset_dict, "contact_email"), - self._get_dataset_value(dataset_dict, "maintainer"), - self._get_dataset_value(dataset_dict, "maintainer_email"), - self._get_dataset_value(dataset_dict, "author"), - self._get_dataset_value(dataset_dict, "author_email"), - ] - ): - - contact_uri = self._get_dataset_value(dataset_dict, "contact_uri") - if contact_uri: - contact_details = CleanedURIRef(contact_uri) - else: - contact_details = BNode() - - g.add((contact_details, RDF.type, VCARD.Organization)) - g.add((dataset_ref, DCAT.contactPoint, contact_details)) - - self._add_triple_from_dict( - dataset_dict, - contact_details, - VCARD.fn, - "contact_name", - ["maintainer", "author"], - ) - # Add mail address as URIRef, and ensure it has a mailto: prefix - self._add_triple_from_dict( - dataset_dict, - contact_details, - VCARD.hasEmail, - "contact_email", - ["maintainer_email", "author_email"], - _type=URIRef, - value_modifier=self._add_mailto, - ) - - # Publisher - publisher_ref = None - - if dataset_dict.get("publisher"): - # Scheming publisher field: will be handled in a separate profile - pass - elif any( - [ - self._get_dataset_value(dataset_dict, "publisher_uri"), - self._get_dataset_value(dataset_dict, "publisher_name"), - ] - ): - # Legacy publisher_* extras - publisher_uri = self._get_dataset_value(dataset_dict, "publisher_uri") - publisher_name = self._get_dataset_value(dataset_dict, "publisher_name") - if publisher_uri: - publisher_ref = CleanedURIRef(publisher_uri) - else: - # No publisher_uri - publisher_ref = BNode() - publisher_details = { - "name": publisher_name, - "email": self._get_dataset_value(dataset_dict, "publisher_email"), - "url": self._get_dataset_value(dataset_dict, "publisher_url"), - "type": self._get_dataset_value(dataset_dict, "publisher_type"), - } - elif dataset_dict.get("organization"): - # Fall back to dataset org - org_id = dataset_dict["organization"]["id"] - org_dict = None - if org_id in self._org_cache: - org_dict = self._org_cache[org_id] - else: - try: - org_dict = toolkit.get_action("organization_show")( - {"ignore_auth": True}, {"id": org_id} - ) - self._org_cache[org_id] = org_dict - except toolkit.ObjectNotFound: - pass - if org_dict: - publisher_ref = CleanedURIRef( - publisher_uri_organization_fallback(dataset_dict) - ) - publisher_details = { - "name": org_dict.get("title"), - "email": org_dict.get("email"), - "url": org_dict.get("url"), - "type": org_dict.get("dcat_type"), - } - # Add to graph - if publisher_ref: - g.add((publisher_ref, RDF.type, FOAF.Agent)) - g.add((dataset_ref, DCT.publisher, publisher_ref)) - items = [ - ("name", FOAF.name, None, Literal), - ("email", FOAF.mbox, None, Literal), - ("url", FOAF.homepage, None, URIRef), - ("type", DCT.type, None, URIRefOrLiteral), - ] - self._add_triples_from_dict(publisher_details, publisher_ref, items) - - # Temporal - start = self._get_dataset_value(dataset_dict, "temporal_start") - end = self._get_dataset_value(dataset_dict, "temporal_end") - if start or end: - temporal_extent = BNode() - - g.add((temporal_extent, RDF.type, DCT.PeriodOfTime)) - if start: - self._add_date_triple(temporal_extent, SCHEMA.startDate, start) - if end: - self._add_date_triple(temporal_extent, SCHEMA.endDate, end) - g.add((dataset_ref, DCT.temporal, temporal_extent)) - - # Spatial - spatial_text = self._get_dataset_value(dataset_dict, "spatial_text") - spatial_geom = self._get_dataset_value(dataset_dict, "spatial") - - if spatial_text or spatial_geom: - spatial_ref = self._get_or_create_spatial_ref(dataset_dict, dataset_ref) - - if spatial_text: - g.add((spatial_ref, SKOS.prefLabel, Literal(spatial_text))) - - if spatial_geom: - self._add_spatial_value_to_graph( - spatial_ref, LOCN.geometry, spatial_geom - ) - - # Use fallback license if set in config - resource_license_fallback = None - if toolkit.asbool(config.get(DISTRIBUTION_LICENSE_FALLBACK_CONFIG, False)): - if "license_id" in dataset_dict and isinstance( - URIRefOrLiteral(dataset_dict["license_id"]), URIRef - ): - resource_license_fallback = dataset_dict["license_id"] - elif "license_url" in dataset_dict and isinstance( - URIRefOrLiteral(dataset_dict["license_url"]), URIRef - ): - resource_license_fallback = dataset_dict["license_url"] - - # Resources - for resource_dict in dataset_dict.get("resources", []): - - distribution = CleanedURIRef(resource_uri(resource_dict)) - - g.add((dataset_ref, DCAT.distribution, 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, DCT.RightsStatement), - ("license", DCT.license, None, URIRefOrLiteral, DCT.LicenseDocument), - ("access_url", DCAT.accessURL, None, URIRef, RDFS.Resource), - ("download_url", DCAT.downloadURL, None, URIRef, RDFS.Resource), - ] - - self._add_triples_from_dict(resource_dict, distribution, items) - - # Lists - items = [ - ("documentation", FOAF.page, None, URIRefOrLiteral, FOAF.Document), - ("language", DCT.language, None, URIRefOrLiteral, DCT.LinguisticSystem), - ("conforms_to", DCT.conformsTo, None, URIRefOrLiteral, DCT.Standard), - ] - 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), - ) - ) - # TODO: add an actual field to manage this - if (distribution, DCT.license, None) in g: - g.add( - ( - list(g.objects(distribution, DCT.license))[0], - DCT.type, - URIRef("http://purl.org/adms/licencetype/UnknownIPR") - ) - ) - - # 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: - mimetype = URIRefOrLiteral(mimetype) - g.add((distribution, DCAT.mediaType, mimetype)) - if isinstance(mimetype, URIRef): - g.add((mimetype, RDF.type, DCT.MediaType)) - - if fmt: - fmt = URIRefOrLiteral(fmt) - g.add((distribution, DCT["format"], fmt)) - if isinstance(fmt, URIRef): - g.add((fmt, RDF.type, DCT.MediaTypeOrExtent)) - - # 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 - ) - - # Dates - items = [ - ("issued", DCT.issued, ["created"], Literal), - ("modified", DCT.modified, ["metadata_modified"], Literal), - ] - - self._add_date_triples_from_dict(resource_dict, distribution, items) - - # Numbers - if resource_dict.get("size"): - try: - g.add( - ( - distribution, - DCAT.byteSize, - Literal(Decimal(resource_dict["size"]), datatype=XSD.decimal), - ) - ) - except (ValueError, TypeError, DecimalException): - 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"): - checksum_algo = URIRefOrLiteral(resource_dict["hash_algorithm"]) - g.add( - ( - checksum, - SPDX.algorithm, - checksum_algo, - ) - ) - if isinstance(checksum_algo, URIRef): - g.add((checksum_algo, RDF.type, SPDX.ChecksumAlgorithm)) - - g.add((distribution, SPDX.checksum, checksum)) + # DCAT AP v1 specific properties + self._graph_from_dataset_v1_only(dataset_dict, dataset_ref) def graph_from_catalog(self, catalog_dict, catalog_ref): - g = self.g - - for prefix, namespace in namespaces.items(): - g.bind(prefix, namespace) - - g.add((catalog_ref, RDF.type, DCAT.Catalog)) - - # Basic fields - items = [ - ("title", DCT.title, config.get("ckan.site_title"), Literal), - ( - "description", - DCT.description, - config.get("ckan.site_description"), - Literal, - ), - ("homepage", FOAF.homepage, config.get("ckan.site_url"), URIRef), - ( - "language", - DCT.language, - config.get("ckan.locale_default", "en"), - URIRefOrLiteral, - ), - ] - for item in items: - key, predicate, fallback, _type = item - if catalog_dict: - value = catalog_dict.get(key, fallback) - else: - value = fallback - if value: - g.add((catalog_ref, predicate, _type(value))) - - # Dates - modified = self._last_catalog_modification() - if modified: - self._add_date_triple(catalog_ref, DCT.modified, modified) + self._graph_from_catalog_base(catalog_dict, catalog_ref) + + def _graph_from_dataset_v1_only(self, dataset_dict, dataset_ref): + """ + CKAN -> DCAT v1 specific properties (not applied to higher versions) + """ + + # Other identifiers (these are handled differently in the + # DCAT-AP v3 profile) + self._add_triple_from_dict( + dataset_dict, + dataset_ref, + ADMS.identifier, + "alternate_identifier", + list_value=True, + _type=URIRefOrLiteral, + _class=ADMS.Identifier, + ) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_2.py b/ckanext/dcat/profiles/euro_dcat_ap_2.py index b5683b1f..b29ac7a4 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_2.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_2.py @@ -7,22 +7,22 @@ from .base import URIRefOrLiteral, CleanedURIRef from .base import ( RDF, - SKOS, DCAT, DCATAP, DCT, XSD, SCHEMA, RDFS, + ADMS, ) -from .euro_dcat_ap import EuropeanDCATAPProfile +from .euro_dcat_ap_base import BaseEuropeanDCATAPProfile ELI = Namespace("http://data.europa.eu/eli/ontology#") -class EuropeanDCATAP2Profile(EuropeanDCATAPProfile): +class EuropeanDCATAP2Profile(BaseEuropeanDCATAPProfile): """ An RDF profile based on the DCAT-AP 2 for data portals in Europe @@ -34,8 +34,36 @@ class EuropeanDCATAP2Profile(EuropeanDCATAPProfile): def parse_dataset(self, dataset_dict, dataset_ref): - # call super method - super(EuropeanDCATAP2Profile, self).parse_dataset(dataset_dict, dataset_ref) + # Call base method for common properties + dataset_dict = self._parse_dataset_base(dataset_dict, dataset_ref) + + # DCAT AP v2 properties also applied to higher versions + dataset_dict = self._parse_dataset_v2(dataset_dict, dataset_ref) + + return dataset_dict + + def graph_from_dataset(self, dataset_dict, dataset_ref): + + # Call base method for common properties + self._graph_from_dataset_base(dataset_dict, dataset_ref) + + # DCAT AP v2 properties also applied to higher versions + self._graph_from_dataset_v2(dataset_dict, dataset_ref) + + # DCAT AP v2 specific properties + self._graph_from_dataset_v2_only(dataset_dict, dataset_ref) + + def graph_from_catalog(self, catalog_dict, catalog_ref): + + self._graph_from_catalog_base(catalog_dict, catalog_ref) + + def _parse_dataset_v2(self, dataset_dict, dataset_ref): + """ + DCAT -> CKAN properties carried forward to higher DCAT-AP versions + """ + + # Call base super method for common properties + super().parse_dataset(dataset_dict, dataset_ref) # Standard values value = self._object_value(dataset_ref, DCAT.temporalResolution) @@ -70,8 +98,8 @@ def parse_dataset(self, dataset_dict, dataset_ref): dataset_ref, DCAT.spatialResolutionInMeters ) if spatial_resolution: - # For some reason we incorrectly allowed lists in this property at some point - # keep support for it but default to single value + # For some reason we incorrectly allowed lists in this property at + # some point, keep support for it but default to single value value = ( spatial_resolution[0] if len(spatial_resolution) == 1 @@ -146,8 +174,9 @@ def parse_dataset(self, dataset_dict, dataset_ref): else "" ) - # Remember the (internal) access service reference for referencing in - # further profiles, e.g. for adding more properties + # Remember the (internal) access service reference for + # referencing in further profiles, e.g. for adding more + # properties access_service_dict["access_service_ref"] = str(access_service) access_service_list.append(access_service_dict) @@ -159,12 +188,10 @@ def parse_dataset(self, dataset_dict, dataset_ref): 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 - ) + def _graph_from_dataset_v2(self, dataset_dict, dataset_ref): + """ + CKAN -> DCAT properties carried forward to higher DCAT-AP versions + """ # Standard values self._add_triple_from_dict( @@ -315,8 +342,8 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): access_service_node = CleanedURIRef(access_service_uri) else: access_service_node = BNode() - # Remember the (internal) access service reference for referencing in - # further profiles + # 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)) @@ -361,9 +388,19 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): if access_service_list: resource_dict["access_services"] = json.dumps(access_service_list) - def graph_from_catalog(self, catalog_dict, catalog_ref): + def _graph_from_dataset_v2_only(self, dataset_dict, dataset_ref): + """ + CKAN -> DCAT v2 specific properties (not applied to higher versions) + """ - # call super method - super(EuropeanDCATAP2Profile, self).graph_from_catalog( - catalog_dict, catalog_ref + # Other identifiers (these are handled differently in the + # DCAT-AP v3 profile) + self._add_triple_from_dict( + dataset_dict, + dataset_ref, + ADMS.identifier, + "alternate_identifier", + list_value=True, + _type=URIRefOrLiteral, + _class=ADMS.Identifier, ) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_3.py b/ckanext/dcat/profiles/euro_dcat_ap_3.py index 44f68c75..d0cce700 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_3.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_3.py @@ -1,17 +1,54 @@ -from rdflib import Literal +from rdflib import Literal, BNode -from ckanext.dcat.profiles import EuropeanDCATAP2Profile, DCAT, XSD +from ckanext.dcat.profiles import ( + DCAT, + XSD, + SKOS, + ADMS, + RDF, +) +from .euro_dcat_ap_2 import EuropeanDCATAP2Profile +from .euro_dcat_ap_scheming import EuropeanDCATAPSchemingProfile -class EuropeanDCATAP3Profile(EuropeanDCATAP2Profile): + +class EuropeanDCATAP3Profile(EuropeanDCATAP2Profile, EuropeanDCATAPSchemingProfile): """ An RDF profile based on the DCAT-AP 3 for data portals in Europe """ + def parse_dataset(self, dataset_dict, dataset_ref): + + # Call base method for common properties + dataset_dict = self._parse_dataset_base(dataset_dict, dataset_ref) + + # DCAT AP v2 properties also applied to higher versions + dataset_dict = self._parse_dataset_v2(dataset_dict, dataset_ref) + + # DCAT AP v2 scheming fields + dataset_dict = self._parse_dataset_v2_scheming(dataset_dict, dataset_ref) + + return dataset_dict + def graph_from_dataset(self, dataset_dict, dataset_ref): - # Call the DCAT AP 2 method - super().graph_from_dataset(dataset_dict, dataset_ref) + # Call base method for common properties + self._graph_from_dataset_base(dataset_dict, dataset_ref) + + # DCAT AP v2 properties also applied to higher versions + self._graph_from_dataset_v2(dataset_dict, dataset_ref) + + # DCAT AP v2 scheming fields + self._graph_from_dataset_v2_scheming(dataset_dict, dataset_ref) + + # DCAT AP v3 properties also applied to higher versions + self._graph_from_dataset_v3(dataset_dict, dataset_ref) + + def graph_from_catalog(self, catalog_dict, catalog_ref): + + self._graph_from_catalog_base(self, catalog_dict, catalog_ref) + + def _graph_from_dataset_v3(self, dataset_dict, dataset_ref): # byteSize decimal -> nonNegativeInteger for subject, predicate, object in self.g.triples((None, DCAT.byteSize, None)): @@ -25,3 +62,13 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): Literal(int(object), datatype=XSD.nonNegativeInteger), ) ) + + # Other identifiers + value = self._get_dict_value(dataset_dict, "alternate_identifier") + if value: + items = self._read_list_value(value) + for item in items: + identifier = BNode() + self.g.add((dataset_ref, ADMS.identifier, identifier)) + self.g.add((identifier, RDF.type, ADMS.Identifier)) + self.g.add((identifier, SKOS.notation, Literal(item))) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_base.py b/ckanext/dcat/profiles/euro_dcat_ap_base.py new file mode 100644 index 00000000..56525a28 --- /dev/null +++ b/ckanext/dcat/profiles/euro_dcat_ap_base.py @@ -0,0 +1,629 @@ +import json +from decimal import Decimal, DecimalException + +from rdflib import term, URIRef, BNode, Literal +import ckantoolkit as toolkit + +from ckan.lib.munge import munge_tag + +from ckanext.dcat.utils import ( + resource_uri, + DCAT_EXPOSE_SUBCATALOGS, + DCAT_CLEAN_TAGS, + publisher_uri_organization_fallback, +) +from .base import RDFProfile, URIRefOrLiteral, CleanedURIRef +from .base import ( + RDF, + XSD, + SKOS, + RDFS, + DCAT, + DCT, + ADMS, + VCARD, + FOAF, + SCHEMA, + LOCN, + GSP, + OWL, + SPDX, + GEOJSON_IMT, + namespaces, +) + +config = toolkit.config + + +DISTRIBUTION_LICENSE_FALLBACK_CONFIG = "ckanext.dcat.resource.inherit.license" + + +class BaseEuropeanDCATAPProfile(RDFProfile): + """ + A base profile with common RDF properties across the different DCAT-AP versions + + """ + + def _parse_dataset_base(self, dataset_dict, dataset_ref): + + dataset_dict["extras"] = [] + dataset_dict["resources"] = [] + + # Basic fields + for key, predicate in ( + ("title", DCT.title), + ("notes", DCT.description), + ("url", DCAT.landingPage), + ("version", OWL.versionInfo), + ): + value = self._object_value(dataset_ref, predicate) + if value: + dataset_dict[key] = value + + if not dataset_dict.get("version"): + # adms:version was supported on the first version of the DCAT-AP + value = self._object_value(dataset_ref, ADMS.version) + if value: + dataset_dict["version"] = value + + # Tags + # replace munge_tag to noop if there's no need to clean tags + do_clean = toolkit.asbool(config.get(DCAT_CLEAN_TAGS, False)) + tags_val = [ + munge_tag(tag) if do_clean else tag for tag in self._keywords(dataset_ref) + ] + tags = [{"name": tag} for tag in tags_val] + dataset_dict["tags"] = tags + + # Extras + + # Simple values + for key, predicate in ( + ("issued", DCT.issued), + ("modified", DCT.modified), + ("identifier", DCT.identifier), + ("version_notes", ADMS.versionNotes), + ("frequency", DCT.accrualPeriodicity), + ("provenance", DCT.provenance), + ("dcat_type", DCT.type), + ): + value = self._object_value(dataset_ref, predicate) + if value: + dataset_dict["extras"].append({"key": key, "value": value}) + + # Lists + for key, predicate, in ( + ("language", DCT.language), + ("theme", DCAT.theme), + ("alternate_identifier", ADMS.identifier), + ("conforms_to", DCT.conformsTo), + ("documentation", FOAF.page), + ("related_resource", DCT.relation), + ("has_version", DCT.hasVersion), + ("is_version_of", DCT.isVersionOf), + ("source", DCT.source), + ("sample", ADMS.sample), + ): + values = self._object_value_list(dataset_ref, predicate) + if values: + dataset_dict["extras"].append({"key": key, "value": json.dumps(values)}) + + # Contact details + contact = self._contact_details(dataset_ref, DCAT.contactPoint) + if not contact: + # adms:contactPoint was supported on the first version of DCAT-AP + contact = self._contact_details(dataset_ref, ADMS.contactPoint) + + if contact: + for key in ("uri", "name", "email"): + if contact.get(key): + dataset_dict["extras"].append( + {"key": "contact_{0}".format(key), "value": contact.get(key)} + ) + + # Publisher + publisher = self._publisher(dataset_ref, DCT.publisher) + for key in ("uri", "name", "email", "url", "type"): + if publisher.get(key): + dataset_dict["extras"].append( + {"key": "publisher_{0}".format(key), "value": publisher.get(key)} + ) + + # Temporal + start, end = self._time_interval(dataset_ref, DCT.temporal) + if start: + dataset_dict["extras"].append({"key": "temporal_start", "value": start}) + if end: + dataset_dict["extras"].append({"key": "temporal_end", "value": end}) + + # Spatial + spatial = self._spatial(dataset_ref, DCT.spatial) + for key in ("uri", "text", "geom"): + self._add_spatial_to_dict(dataset_dict, key, spatial) + + # Dataset URI (explicitly show the missing ones) + dataset_uri = str(dataset_ref) if isinstance(dataset_ref, term.URIRef) else "" + dataset_dict["extras"].append({"key": "uri", "value": dataset_uri}) + + # access_rights + access_rights = self._access_rights(dataset_ref, DCT.accessRights) + if access_rights: + dataset_dict["extras"].append( + {"key": "access_rights", "value": access_rights} + ) + + # License + if "license_id" not in dataset_dict: + dataset_dict["license_id"] = self._license(dataset_ref) + + # Source Catalog + if toolkit.asbool(config.get(DCAT_EXPOSE_SUBCATALOGS, False)): + catalog_src = self._get_source_catalog(dataset_ref) + if catalog_src is not None: + src_data = self._extract_catalog_dict(catalog_src) + dataset_dict["extras"].extend(src_data) + + # Resources + for distribution in self._distributions(dataset_ref): + + resource_dict = {} + + # Simple values + for key, predicate in ( + ("name", DCT.title), + ("description", DCT.description), + ("access_url", DCAT.accessURL), + ("download_url", DCAT.downloadURL), + ("issued", DCT.issued), + ("modified", DCT.modified), + ("status", ADMS.status), + ("license", DCT.license), + ): + value = self._object_value(distribution, predicate) + if value: + resource_dict[key] = value + + resource_dict["url"] = self._object_value( + distribution, DCAT.downloadURL + ) or self._object_value(distribution, DCAT.accessURL) + # Lists + for key, predicate in ( + ("language", DCT.language), + ("documentation", FOAF.page), + ("conforms_to", DCT.conformsTo), + ): + values = self._object_value_list(distribution, predicate) + if values: + resource_dict[key] = json.dumps(values) + + # rights + rights = self._access_rights(distribution, DCT.rights) + if rights: + resource_dict["rights"] = rights + + # Format and media type + normalize_ckan_format = toolkit.asbool( + config.get("ckanext.dcat.normalize_ckan_format", True) + ) + imt, label = self._distribution_format(distribution, normalize_ckan_format) + + if imt: + resource_dict["mimetype"] = imt + + if label: + resource_dict["format"] = label + elif imt: + resource_dict["format"] = imt + + # Size + size = self._object_value_int(distribution, DCAT.byteSize) + if size is not None: + resource_dict["size"] = size + + # Checksum + for checksum in self.g.objects(distribution, SPDX.checksum): + algorithm = self._object_value(checksum, SPDX.algorithm) + checksum_value = self._object_value(checksum, SPDX.checksumValue) + if algorithm: + resource_dict["hash_algorithm"] = algorithm + if checksum_value: + resource_dict["hash"] = checksum_value + + # Distribution URI (explicitly show the missing ones) + resource_dict["uri"] = ( + str(distribution) if isinstance(distribution, term.URIRef) else "" + ) + + # Remember the (internal) distribution reference for referencing in + # further profiles, e.g. for adding more properties + resource_dict["distribution_ref"] = str(distribution) + + dataset_dict["resources"].append(resource_dict) + + if self.compatibility_mode: + # Tweak the resulting dict to make it compatible with previous + # versions of the ckanext-dcat parsers + for extra in dataset_dict["extras"]: + if extra["key"] in ( + "issued", + "modified", + "publisher_name", + "publisher_email", + ): + + extra["key"] = "dcat_" + extra["key"] + + if extra["key"] == "language": + extra["value"] = ",".join(sorted(json.loads(extra["value"]))) + + return dataset_dict + + def _graph_from_dataset_base(self, dataset_dict, dataset_ref): + + g = self.g + + for prefix, namespace in namespaces.items(): + g.bind(prefix, namespace) + + g.add((dataset_ref, RDF.type, DCAT.Dataset)) + + # Basic fields + items = [ + ("title", DCT.title, None, Literal), + ("notes", DCT.description, None, Literal), + ("url", DCAT.landingPage, None, URIRef, FOAF.Document), + ("identifier", DCT.identifier, ["guid", "id"], URIRefOrLiteral), + ("version", OWL.versionInfo, ["dcat_version"], Literal), + ("version_notes", ADMS.versionNotes, None, Literal), + ("frequency", DCT.accrualPeriodicity, None, URIRefOrLiteral, DCT.Frequency), + ("access_rights", DCT.accessRights, None, URIRefOrLiteral, DCT.AccessRights), + ("dcat_type", DCT.type, None, URIRefOrLiteral), + ("provenance", DCT.provenance, None, URIRefOrLiteral, DCT.ProvenanceStatement), + ] + self._add_triples_from_dict(dataset_dict, dataset_ref, items) + + # Tags + for tag in dataset_dict.get("tags", []): + g.add((dataset_ref, DCAT.keyword, Literal(tag["name"]))) + + # Dates + items = [ + ("issued", DCT.issued, ["metadata_created"], Literal), + ("modified", DCT.modified, ["metadata_modified"], Literal), + ] + self._add_date_triples_from_dict(dataset_dict, dataset_ref, items) + + # Lists + items = [ + ("language", DCT.language, None, URIRefOrLiteral, DCT.LinguisticSystem), + ("theme", DCAT.theme, None, URIRef), + ("conforms_to", DCT.conformsTo, None, URIRefOrLiteral, DCT.Standard), + ("documentation", FOAF.page, None, URIRefOrLiteral, FOAF.Document), + ("related_resource", DCT.relation, None, URIRefOrLiteral, RDFS.Resource), + ("has_version", DCT.hasVersion, None, URIRefOrLiteral), + ("is_version_of", DCT.isVersionOf, None, URIRefOrLiteral), + ("source", DCT.source, None, URIRefOrLiteral), + ("sample", ADMS.sample, None, URIRefOrLiteral, DCAT.Distribution), + ] + self._add_list_triples_from_dict(dataset_dict, dataset_ref, items) + + # Contact details + if any( + [ + self._get_dataset_value(dataset_dict, "contact_uri"), + self._get_dataset_value(dataset_dict, "contact_name"), + self._get_dataset_value(dataset_dict, "contact_email"), + self._get_dataset_value(dataset_dict, "maintainer"), + self._get_dataset_value(dataset_dict, "maintainer_email"), + self._get_dataset_value(dataset_dict, "author"), + self._get_dataset_value(dataset_dict, "author_email"), + ] + ): + + contact_uri = self._get_dataset_value(dataset_dict, "contact_uri") + if contact_uri: + contact_details = CleanedURIRef(contact_uri) + else: + contact_details = BNode() + + g.add((contact_details, RDF.type, VCARD.Organization)) + g.add((dataset_ref, DCAT.contactPoint, contact_details)) + + self._add_triple_from_dict( + dataset_dict, + contact_details, + VCARD.fn, + "contact_name", + ["maintainer", "author"], + ) + # Add mail address as URIRef, and ensure it has a mailto: prefix + self._add_triple_from_dict( + dataset_dict, + contact_details, + VCARD.hasEmail, + "contact_email", + ["maintainer_email", "author_email"], + _type=URIRef, + value_modifier=self._add_mailto, + ) + + # Publisher + publisher_ref = None + + if dataset_dict.get("publisher"): + # Scheming publisher field: will be handled in a separate profile + pass + elif any( + [ + self._get_dataset_value(dataset_dict, "publisher_uri"), + self._get_dataset_value(dataset_dict, "publisher_name"), + ] + ): + # Legacy publisher_* extras + publisher_uri = self._get_dataset_value(dataset_dict, "publisher_uri") + publisher_name = self._get_dataset_value(dataset_dict, "publisher_name") + if publisher_uri: + publisher_ref = CleanedURIRef(publisher_uri) + else: + # No publisher_uri + publisher_ref = BNode() + publisher_details = { + "name": publisher_name, + "email": self._get_dataset_value(dataset_dict, "publisher_email"), + "url": self._get_dataset_value(dataset_dict, "publisher_url"), + "type": self._get_dataset_value(dataset_dict, "publisher_type"), + } + elif dataset_dict.get("organization"): + # Fall back to dataset org + org_id = dataset_dict["organization"]["id"] + org_dict = None + if org_id in self._org_cache: + org_dict = self._org_cache[org_id] + else: + try: + org_dict = toolkit.get_action("organization_show")( + {"ignore_auth": True}, {"id": org_id} + ) + self._org_cache[org_id] = org_dict + except toolkit.ObjectNotFound: + pass + if org_dict: + publisher_ref = CleanedURIRef( + publisher_uri_organization_fallback(dataset_dict) + ) + publisher_details = { + "name": org_dict.get("title"), + "email": org_dict.get("email"), + "url": org_dict.get("url"), + "type": org_dict.get("dcat_type"), + } + # Add to graph + if publisher_ref: + g.add((publisher_ref, RDF.type, FOAF.Agent)) + g.add((dataset_ref, DCT.publisher, publisher_ref)) + items = [ + ("name", FOAF.name, None, Literal), + ("email", FOAF.mbox, None, Literal), + ("url", FOAF.homepage, None, URIRef), + ("type", DCT.type, None, URIRefOrLiteral), + ] + self._add_triples_from_dict(publisher_details, publisher_ref, items) + + # Temporal + start = self._get_dataset_value(dataset_dict, "temporal_start") + end = self._get_dataset_value(dataset_dict, "temporal_end") + if start or end: + temporal_extent = BNode() + + g.add((temporal_extent, RDF.type, DCT.PeriodOfTime)) + if start: + self._add_date_triple(temporal_extent, SCHEMA.startDate, start) + if end: + self._add_date_triple(temporal_extent, SCHEMA.endDate, end) + g.add((dataset_ref, DCT.temporal, temporal_extent)) + + # Spatial + spatial_text = self._get_dataset_value(dataset_dict, "spatial_text") + spatial_geom = self._get_dataset_value(dataset_dict, "spatial") + + if spatial_text or spatial_geom: + spatial_ref = self._get_or_create_spatial_ref(dataset_dict, dataset_ref) + + if spatial_text: + g.add((spatial_ref, SKOS.prefLabel, Literal(spatial_text))) + + if spatial_geom: + self._add_spatial_value_to_graph( + spatial_ref, LOCN.geometry, spatial_geom + ) + + # Use fallback license if set in config + resource_license_fallback = None + if toolkit.asbool(config.get(DISTRIBUTION_LICENSE_FALLBACK_CONFIG, False)): + if "license_id" in dataset_dict and isinstance( + URIRefOrLiteral(dataset_dict["license_id"]), URIRef + ): + resource_license_fallback = dataset_dict["license_id"] + elif "license_url" in dataset_dict and isinstance( + URIRefOrLiteral(dataset_dict["license_url"]), URIRef + ): + resource_license_fallback = dataset_dict["license_url"] + + # Resources + for resource_dict in dataset_dict.get("resources", []): + + distribution = CleanedURIRef(resource_uri(resource_dict)) + + g.add((dataset_ref, DCAT.distribution, 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, DCT.RightsStatement), + ("license", DCT.license, None, URIRefOrLiteral, DCT.LicenseDocument), + ("access_url", DCAT.accessURL, None, URIRef, RDFS.Resource), + ("download_url", DCAT.downloadURL, None, URIRef, RDFS.Resource), + ] + + self._add_triples_from_dict(resource_dict, distribution, items) + + # Lists + items = [ + ("documentation", FOAF.page, None, URIRefOrLiteral, FOAF.Document), + ("language", DCT.language, None, URIRefOrLiteral, DCT.LinguisticSystem), + ("conforms_to", DCT.conformsTo, None, URIRefOrLiteral, DCT.Standard), + ] + 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), + ) + ) + # TODO: add an actual field to manage this + if (distribution, DCT.license, None) in g: + g.add( + ( + list(g.objects(distribution, DCT.license))[0], + DCT.type, + URIRef("http://purl.org/adms/licencetype/UnknownIPR") + ) + ) + + # 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: + mimetype = URIRefOrLiteral(mimetype) + g.add((distribution, DCAT.mediaType, mimetype)) + if isinstance(mimetype, URIRef): + g.add((mimetype, RDF.type, DCT.MediaType)) + + if fmt: + fmt = URIRefOrLiteral(fmt) + g.add((distribution, DCT["format"], fmt)) + if isinstance(fmt, URIRef): + g.add((fmt, RDF.type, DCT.MediaTypeOrExtent)) + + # 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 + ) + + # Dates + items = [ + ("issued", DCT.issued, ["created"], Literal), + ("modified", DCT.modified, ["metadata_modified"], Literal), + ] + + self._add_date_triples_from_dict(resource_dict, distribution, items) + + # Numbers + if resource_dict.get("size"): + try: + g.add( + ( + distribution, + DCAT.byteSize, + Literal(Decimal(resource_dict["size"]), datatype=XSD.decimal), + ) + ) + except (ValueError, TypeError, DecimalException): + 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"): + checksum_algo = URIRefOrLiteral(resource_dict["hash_algorithm"]) + g.add( + ( + checksum, + SPDX.algorithm, + checksum_algo, + ) + ) + if isinstance(checksum_algo, URIRef): + g.add((checksum_algo, RDF.type, SPDX.ChecksumAlgorithm)) + + g.add((distribution, SPDX.checksum, checksum)) + + def _graph_from_catalog_base(self, catalog_dict, catalog_ref): + + g = self.g + + for prefix, namespace in namespaces.items(): + g.bind(prefix, namespace) + + g.add((catalog_ref, RDF.type, DCAT.Catalog)) + + # Basic fields + items = [ + ("title", DCT.title, config.get("ckan.site_title"), Literal), + ( + "description", + DCT.description, + config.get("ckan.site_description"), + Literal, + ), + ("homepage", FOAF.homepage, config.get("ckan.site_url"), URIRef), + ( + "language", + DCT.language, + config.get("ckan.locale_default", "en"), + URIRefOrLiteral, + ), + ] + for item in items: + key, predicate, fallback, _type = item + if catalog_dict: + value = catalog_dict.get(key, fallback) + else: + value = fallback + if value: + g.add((catalog_ref, predicate, _type(value))) + + # Dates + modified = self._last_catalog_modification() + if modified: + self._add_date_triple(catalog_ref, DCT.modified, modified) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py index f9d5e977..ef90c930 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py @@ -4,12 +4,10 @@ from .base import RDFProfile, CleanedURIRef, URIRefOrLiteral from .base import ( RDF, - XSD, DCAT, DCT, VCARD, FOAF, - SCHEMA, SKOS, LOCN, ) @@ -17,16 +15,24 @@ class EuropeanDCATAPSchemingProfile(RDFProfile): """ - This is a compatibilty profile meant to add support for ckanext-scheming to the existing - `euro_dcat_ap` and `euro_dcat_ap_2` profiles. + This is a compatibilty profile meant to add support for ckanext-scheming to the + existing `euro_dcat_ap` and `euro_dcat_ap_2` profiles. It does not add or remove any properties from these profiles, it just transforms the resulting dataset_dict so it is compatible with a ckanext-scheming schema """ def parse_dataset(self, dataset_dict, dataset_ref): + + return self._parse_dataset_v2_scheming(dataset_dict, dataset_ref) + + def graph_from_dataset(self, dataset_dict, dataset_ref): + + self._graph_from_dataset_v2_scheming(dataset_dict, dataset_ref) + + def _parse_dataset_v2_scheming(self, dataset_dict, dataset_ref): """ - Modify the dataset_dict generated by the euro_dcat_ap andeuro_dcat_ap_2 profiles - to make it compatible with the scheming file definitions: + Modify the dataset_dict generated by the euro_dcat_ap and euro_dcat_ap_2 + profiles to make it compatible with the scheming file definitions: * Move extras to root level fields * Parse lists (multiple text preset) * Turn namespaced extras into repeating subfields @@ -81,7 +87,7 @@ def _parse_list_value(data_dict, field_name): check_name = new_fields_mapping.get(field_name, field_name) for extra in dataset_dict.get("extras", []): if extra["key"].startswith(f"{check_name}_"): - subfield = extra["key"][extra["key"].index("_") + 1 :] + subfield = extra["key"][extra["key"].index("_") + 1:] if subfield in [ f["field_name"] for f in schema_field["repeating_subfields"] ]: @@ -113,7 +119,7 @@ def _parse_list_value(data_dict, field_name): return dataset_dict - def graph_from_dataset(self, dataset_dict, dataset_ref): + def _graph_from_dataset_v2_scheming(self, dataset_dict, dataset_ref): """ Add triples to the graph from new repeating subfields """ diff --git a/ckanext/dcat/tests/test_euro_dcatap_profile_serialize.py b/ckanext/dcat/tests/test_euro_dcatap_profile_serialize.py index 5583b0ca..ddbf9e21 100644 --- a/ckanext/dcat/tests/test_euro_dcatap_profile_serialize.py +++ b/ckanext/dcat/tests/test_euro_dcatap_profile_serialize.py @@ -22,7 +22,7 @@ DCAT, DCT, ADMS, XSD, VCARD, FOAF, SCHEMA, SKOS, LOCN, GSP, OWL, SPDX, GEOJSON_IMT, ) -from ckanext.dcat.profiles.euro_dcat_ap import DISTRIBUTION_LICENSE_FALLBACK_CONFIG +from ckanext.dcat.profiles.euro_dcat_ap_base import DISTRIBUTION_LICENSE_FALLBACK_CONFIG from ckanext.dcat.utils import DCAT_EXPOSE_SUBCATALOGS from ckanext.dcat.tests.utils import BaseSerializeTest diff --git a/ckanext/dcat/tests/test_shacl.py b/ckanext/dcat/tests/test_shacl.py index ceb4f626..7ba36bee 100644 --- a/ckanext/dcat/tests/test_shacl.py +++ b/ckanext/dcat/tests/test_shacl.py @@ -140,6 +140,7 @@ def test_validate_dcat_ap_2_graph_shapes_range(): graph = graph_from_dataset("ckan_full_dataset_dcat_ap_2_vocabularies.json") + graph.serialize(destination="graph_v2_range.ttl") # dcat-ap_2.1.1_shacl_range.ttl: constraints concerning object range path = _get_shacl_file_path("dcat-ap_2.1.1_shacl_range.ttl") r = validate(graph, shacl_graph=path) @@ -186,10 +187,21 @@ def test_validate_dcat_ap_3_graph(): with open("shacl_results.txt", "w") as f: f.write(results_text) + failures = [ + str(t[2]) + for t in results_graph.triples( + ( + None, + URIRef("http://www.w3.org/ns/shacl#resultMessage"), + None, + ) + ) + ] - assert conforms, results_text - - with open("shacl_results.txt", "w") as f: - f.write(results_text) + known_failures = [ + "Value does not have class skos:Concept", + "Value does not have class dcat:Dataset", + ] + assert set(failures) - set(known_failures) == set(), results_text From f88c97b6d2a3c1d269b3fb70cad100d974057075 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:40:27 +0200 Subject: [PATCH 08/14] Update class for location, LOCN.geometry -> LOCN.Geometry --- ckanext/dcat/profiles/euro_dcat_ap_scheming.py | 2 +- ckanext/dcat/tests/test_scheming_support.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py index ef90c930..67392b6c 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py @@ -216,7 +216,7 @@ def _not_empty_dict(data_dict): self.g.add((spatial_ref, SKOS.prefLabel, Literal(item["text"]))) for field in [ - ("geom", LOCN.geometry), + ("geom", LOCN.Geometry), ("bbox", DCAT.bbox), ("centroid", DCAT.centroid), ]: diff --git a/ckanext/dcat/tests/test_scheming_support.py b/ckanext/dcat/tests/test_scheming_support.py index fc9ad9bf..93238c27 100644 --- a/ckanext/dcat/tests/test_scheming_support.py +++ b/ckanext/dcat/tests/test_scheming_support.py @@ -346,7 +346,7 @@ def test_e2e_ckan_to_dcat(self): assert len([t for t in g.triples((spatial[0][2], LOCN.geometry, None))]) == 1 # Geometry in WKT wkt_geom = wkt.dumps(dataset["spatial_coverage"][0]["geom"], decimals=4) - assert self._triple(g, spatial[0][2], LOCN.geometry, wkt_geom, GSP.wktLiteral) + assert self._triple(g, spatial[0][2], LOCN.Geometry, wkt_geom, GSP.wktLiteral) distribution_ref = self._triple(g, dataset_ref, DCAT.distribution, None)[2] resource = dataset_dict["resources"][0] From cffbf0961adf99cb947ca7669d82062172bd107f Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:45:44 +0200 Subject: [PATCH 09/14] Disable SKOS.Concept for now --- ckanext/dcat/profiles/euro_dcat_ap_scheming.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py index 67392b6c..acf5fa2d 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_scheming.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_scheming.py @@ -176,7 +176,8 @@ def _not_empty_dict(data_dict): DCT.type, "type", _type=URIRefOrLiteral, - _class=SKOS.Concept, + # TODO: fix prefLabel stuff + # _class=SKOS.Concept, ) self._add_triple_from_dict( publisher, From 38c49a56facb79e00c3d1fc8397153c48fd6bbc3 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:46:36 +0200 Subject: [PATCH 10/14] Remove debug commands --- ckanext/dcat/tests/test_shacl.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ckanext/dcat/tests/test_shacl.py b/ckanext/dcat/tests/test_shacl.py index 7ba36bee..a5494138 100644 --- a/ckanext/dcat/tests/test_shacl.py +++ b/ckanext/dcat/tests/test_shacl.py @@ -140,7 +140,6 @@ def test_validate_dcat_ap_2_graph_shapes_range(): graph = graph_from_dataset("ckan_full_dataset_dcat_ap_2_vocabularies.json") - graph.serialize(destination="graph_v2_range.ttl") # dcat-ap_2.1.1_shacl_range.ttl: constraints concerning object range path = _get_shacl_file_path("dcat-ap_2.1.1_shacl_range.ttl") r = validate(graph, shacl_graph=path) @@ -174,18 +173,14 @@ def test_validate_dcat_ap_2_graph_shapes_range(): "scheming.presets", "ckanext.scheming:presets.json ckanext.dcat.schemas:presets.yaml", ) -@pytest.mark.ckan_config( - "ckanext.dcat.rdf.profiles", "euro_dcat_ap_3" -) +@pytest.mark.ckan_config("ckanext.dcat.rdf.profiles", "euro_dcat_ap_3") def test_validate_dcat_ap_3_graph(): graph = graph_from_dataset("ckan_full_dataset_dcat_ap_2_vocabularies.json") - graph.serialize(destination="graph.ttl") + path = _get_shacl_file_path("dcat-ap_3_shacl_shapes.ttl") r = validate(graph, shacl_graph=path) conforms, results_graph, results_text = r - with open("shacl_results.txt", "w") as f: - f.write(results_text) failures = [ str(t[2]) @@ -203,5 +198,4 @@ def test_validate_dcat_ap_3_graph(): "Value does not have class dcat:Dataset", ] - assert set(failures) - set(known_failures) == set(), results_text From c8b87f3a72bae7a74c0f102b26b9e5c7f885450e Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 26 Aug 2024 14:53:09 +0200 Subject: [PATCH 11/14] Fix test --- ckanext/dcat/tests/test_scheming_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/dcat/tests/test_scheming_support.py b/ckanext/dcat/tests/test_scheming_support.py index 93238c27..fc55f700 100644 --- a/ckanext/dcat/tests/test_scheming_support.py +++ b/ckanext/dcat/tests/test_scheming_support.py @@ -343,7 +343,7 @@ def test_e2e_ckan_to_dcat(self): g, spatial[0][2], SKOS.prefLabel, dataset["spatial_coverage"][0]["text"] ) - assert len([t for t in g.triples((spatial[0][2], LOCN.geometry, None))]) == 1 + assert len([t for t in g.triples((spatial[0][2], LOCN.Geometry, None))]) == 1 # Geometry in WKT wkt_geom = wkt.dumps(dataset["spatial_coverage"][0]["geom"], decimals=4) assert self._triple(g, spatial[0][2], LOCN.Geometry, wkt_geom, GSP.wktLiteral) From 167c80d63947de015b87047328175babab325393 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 27 Aug 2024 16:04:47 +0200 Subject: [PATCH 12/14] Add serialize and parse tests for DCAT AP 3 --- .../dcat_ap_2/test_scheming_support.py | 1 - .../test_euro_dcatap_3_profile_parse.py | 159 ++++++ .../test_euro_dcatap_3_profile_serialize.py | 483 ++++++++++++++++++ .../test_euro_dcatap_3_profile_serialize.py | 34 -- 4 files changed, 642 insertions(+), 35 deletions(-) create mode 100644 ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py create mode 100644 ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py delete mode 100644 ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py diff --git a/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py b/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py index fc55f700..de8a8a88 100644 --- a/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py +++ b/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py @@ -24,7 +24,6 @@ LOCN, GSP, OWL, - GEOJSON_IMT, SPDX, ) from ckanext.dcat.tests.utils import BaseSerializeTest, BaseParseTest diff --git a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py new file mode 100644 index 00000000..286a9692 --- /dev/null +++ b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py @@ -0,0 +1,159 @@ +import pytest + +from ckan.tests.helpers import call_action + +from ckanext.dcat.processors import RDFParser +from ckanext.dcat.tests.utils import BaseParseTest + + +@pytest.mark.usefixtures("with_plugins", "clean_db") +@pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") +@pytest.mark.ckan_config( + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" +) +@pytest.mark.ckan_config( + "scheming.presets", + "ckanext.scheming:presets.json ckanext.dcat.schemas:presets.yaml", +) +@pytest.mark.ckan_config("ckanext.dcat.rdf.profiles", "euro_dcat_ap_3") +class TestSchemingParseSupport(BaseParseTest): + def test_e2e_dcat_to_ckan(self): + """ + Parse a DCAT RDF graph into a CKAN dataset dict, create a dataset with + package_create and check that all expected fields are there + """ + contents = self._get_file_contents("dcat/dataset.rdf") + + p = RDFParser() + + p.parse(contents) + + datasets = [d for d in p.datasets()] + + assert len(datasets) == 1 + + dataset_dict = datasets[0] + + dataset_dict["name"] = "test-dcat-1" + dataset = call_action("package_create", **dataset_dict) + + # Core fields + + assert dataset["title"] == "Zimbabwe Regional Geochemical Survey." + assert ( + dataset["notes"] + == "During the period 1982-86 a team of geologists from the British Geological Survey ..." + ) + assert dataset["url"] == "http://dataset.info.org" + assert dataset["version"] == "2.3" + assert dataset["license_id"] == "cc-nc" + assert sorted([t["name"] for t in dataset["tags"]]) == [ + "exploration", + "geochemistry", + "geology", + ] + + # Standard fields + assert dataset["version_notes"] == "New schema added" + assert dataset["identifier"] == u"9df8df51-63db-37a8-e044-0003ba9b0d98" + assert dataset["frequency"] == "http://purl.org/cld/freq/daily" + assert dataset["access_rights"] == "public" + assert dataset["provenance"] == "Some statement about provenance" + assert dataset["dcat_type"] == "test-type" + + assert dataset["issued"] == u"2012-05-10" + assert dataset["modified"] == u"2012-05-10T21:04:00" + assert dataset["temporal_resolution"] == "PT15M" + assert dataset["spatial_resolution_in_meters"] == "1.5" + + # List fields + assert sorted(dataset["conforms_to"]) == ["Standard 1", "Standard 2"] + assert sorted(dataset["language"]) == ["ca", "en", "es"] + assert sorted(dataset["theme"]) == [ + "Earth Sciences", + "http://eurovoc.europa.eu/100142", + "http://eurovoc.europa.eu/209065", + ] + assert sorted(dataset["alternate_identifier"]) == [ + "alternate-identifier-1", + "alternate-identifier-2", + ] + assert sorted(dataset["documentation"]) == [ + "http://dataset.info.org/doc1", + "http://dataset.info.org/doc2", + ] + + assert sorted(dataset["is_referenced_by"]) == [ + "https://doi.org/10.1038/sdata.2018.22", + "test_isreferencedby", + ] + assert sorted(dataset["applicable_legislation"]) == [ + "http://data.europa.eu/eli/reg_impl/2023/138/oj", + "http://data.europa.eu/eli/reg_impl/2023/138/oj_alt", + ] + # Repeating subfields + + assert dataset["contact"][0]["name"] == "Point of Contact" + assert dataset["contact"][0]["email"] == "contact@some.org" + + assert ( + dataset["publisher"][0]["name"] == "Publishing Organization for dataset 1" + ) + assert dataset["publisher"][0]["email"] == "contact@some.org" + assert dataset["publisher"][0]["url"] == "http://some.org" + assert ( + dataset["publisher"][0]["type"] + == "http://purl.org/adms/publishertype/NonProfitOrganisation" + ) + assert dataset["temporal_coverage"][0]["start"] == "1905-03-01" + assert dataset["temporal_coverage"][0]["end"] == "2013-01-05" + + resource = dataset["resources"][0] + + # Resources: core fields + assert resource["url"] == "http://www.bgs.ac.uk/gbase/geochemcd/home.html" + + # Resources: standard fields + assert resource["license"] == "http://creativecommons.org/licenses/by-nc/2.0/" + assert resource["rights"] == "Some statement about rights" + assert resource["issued"] == "2012-05-11" + assert resource["modified"] == "2012-05-01T00:04:06" + assert resource["status"] == "http://purl.org/adms/status/Completed" + assert resource["size"] == 12323 + assert ( + resource["availability"] + == "http://publications.europa.eu/resource/authority/planned-availability/EXPERIMENTAL" + ) + assert ( + resource["compress_format"] + == "http://www.iana.org/assignments/media-types/application/gzip" + ) + assert ( + resource["package_format"] + == "http://publications.europa.eu/resource/authority/file-type/TAR" + ) + + assert resource["hash"] == "4304cf2e751e6053c90b1804c89c0ebb758f395a" + assert ( + resource["hash_algorithm"] + == "http://spdx.org/rdf/terms#checksumAlgorithm_sha1" + ) + + assert ( + resource["access_url"] == "http://www.bgs.ac.uk/gbase/geochemcd/home.html" + ) + assert "download_url" not in resource + + # Resources: list fields + assert sorted(resource["language"]) == ["ca", "en", "es"] + assert sorted(resource["documentation"]) == [ + "http://dataset.info.org/distribution1/doc1", + "http://dataset.info.org/distribution1/doc2", + ] + assert sorted(resource["conforms_to"]) == ["Standard 1", "Standard 2"] + + # Resources: repeating subfields + assert resource["access_services"][0]["title"] == "Sparql-end Point" + assert resource["access_services"][0]["endpoint_url"] == [ + "http://publications.europa.eu/webapi/rdf/sparql" + ] diff --git a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py new file mode 100644 index 00000000..382d6c76 --- /dev/null +++ b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py @@ -0,0 +1,483 @@ +import pytest + +from rdflib.namespace import RDF +from rdflib.term import URIRef +from geomet import wkt + +from ckan.tests.helpers import call_action +from ckanext.dcat.processors import RDFSerializer +from ckanext.dcat.tests.utils import BaseSerializeTest + +from ckanext.dcat import utils +from ckanext.dcat.profiles import ( + RDF, + DCAT, + DCATAP, + DCT, + ADMS, + XSD, + VCARD, + FOAF, + SKOS, + LOCN, + GSP, + OWL, + SPDX, +) + +DCAT_AP_PROFILES = ["euro_dcat_ap_3"] + + +class TestEuroDCATAP3ProfileSerializeDataset(BaseSerializeTest): + @pytest.mark.usefixtures("with_plugins", "clean_db") + @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") + @pytest.mark.ckan_config( + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" + ) + @pytest.mark.ckan_config( + "scheming.presets", + "ckanext.scheming:presets.json ckanext.dcat.schemas:presets.yaml", + ) + @pytest.mark.ckan_config("ckanext.dcat.rdf.profiles", "euro_dcat_ap_3") + def test_e2e_ckan_to_dcat(self): + """ + Create a dataset using the scheming schema, check that fields + are exposed in the DCAT RDF graph + """ + + dataset_dict = { + # Core fields + "name": "test-dataset", + "title": "Test DCAT dataset", + "notes": "Lorem ipsum", + "url": "http://example.org/ds1", + "version": "1.0b", + "tags": [{"name": "Tag 1"}, {"name": "Tag 2"}], + # Standard fields + "issued": "2024-05-01", + "modified": "2024-05-05", + "identifier": "xx-some-dataset-id-yy", + "frequency": "monthly", + "provenance": "Statement about provenance", + "dcat_type": "test-type", + "version_notes": "Some version notes", + "access_rights": "Statement about access rights", + # List fields (lists) + "alternate_identifier": ["alt-id-1", "alt-id-2"], + "theme": [ + "https://example.org/uri/theme1", + "https://example.org/uri/theme2", + "https://example.org/uri/theme3", + ], + "language": ["en", "ca", "es"], + "documentation": ["https://example.org/some-doc.html"], + "conforms_to": ["Standard 1", "Standard 2"], + "is_referenced_by": [ + "https://doi.org/10.1038/sdata.2018.22", + "test_isreferencedby", + ], + "applicable_legislation": [ + "http://data.europa.eu/eli/reg_impl/2023/138/oj", + "http://data.europa.eu/eli/reg_impl/2023/138/oj_alt", + ], + # Repeating subfields + "contact": [ + {"name": "Contact 1", "email": "contact1@example.org"}, + {"name": "Contact 2", "email": "contact2@example.org"}, + ], + "publisher": [ + { + "name": "Test Publisher", + "email": "publisher@example.org", + "url": "https://example.org", + "type": "public_body", + }, + ], + "temporal_coverage": [ + {"start": "1905-03-01", "end": "2013-01-05"}, + {"start": "2024-04-10", "end": "2024-05-29"}, + ], + "temporal_resolution": "PT15M", + "spatial_coverage": [ + { + "geom": { + "type": "Polygon", + "coordinates": [ + [ + [11.9936, 54.0486], + [11.9936, 54.2466], + [12.3045, 54.2466], + [12.3045, 54.0486], + [11.9936, 54.0486], + ] + ], + }, + "text": "Tarragona", + "uri": "https://sws.geonames.org/6361390/", + "bbox": { + "type": "Polygon", + "coordinates": [ + [ + [-2.1604, 42.7611], + [-2.0938, 42.7611], + [-2.0938, 42.7931], + [-2.1604, 42.7931], + [-2.1604, 42.7611], + ] + ], + }, + "centroid": {"type": "Point", "coordinates": [1.26639, 41.12386]}, + } + ], + "spatial_resolution_in_meters": 1.5, + "resources": [ + { + "name": "Resource 1", + "description": "Some description", + "url": "https://example.com/data.csv", + "format": "CSV", + "availability": "http://publications.europa.eu/resource/authority/planned-availability/EXPERIMENTAL", + "compress_format": "http://www.iana.org/assignments/media-types/application/gzip", + "package_format": "http://publications.europa.eu/resource/authority/file-type/TAR", + "size": 12323, + "hash": "4304cf2e751e6053c90b1804c89c0ebb758f395a", + "hash_algorithm": "http://spdx.org/rdf/terms#checksumAlgorithm_sha1", + "status": "http://purl.org/adms/status/Completed", + "access_url": "https://example.com/data.csv", + "download_url": "https://example.com/data.csv", + "issued": "2024-05-01T01:20:33", + "modified": "2024-05-05T09:33:20", + "license": "http://creativecommons.org/licenses/by/3.0/", + "rights": "Some stament about rights", + "language": ["en", "ca", "es"], + "access_services": [ + { + "title": "Access Service 1", + "endpoint_url": [ + "https://example.org/access_service/1", + "https://example.org/access_service/2", + ], + } + ], + } + ], + } + + dataset = call_action("package_create", **dataset_dict) + + # Make sure schema was used + assert dataset["conforms_to"][0] == "Standard 1" + assert dataset["contact"][0]["name"] == "Contact 1" + + s = RDFSerializer() + g = s.g + + dataset_ref = s.graph_from_dataset(dataset) + + assert str(dataset_ref) == utils.dataset_uri(dataset) + + # Core fields + assert self._triple(g, dataset_ref, RDF.type, DCAT.Dataset) + assert self._triple(g, dataset_ref, DCT.title, dataset["title"]) + assert self._triple(g, dataset_ref, DCT.description, dataset["notes"]) + assert self._triple(g, dataset_ref, OWL.versionInfo, dataset["version"]) + + # Standard fields + assert self._triple(g, dataset_ref, DCT.identifier, dataset["identifier"]) + assert self._triple( + g, dataset_ref, DCT.accrualPeriodicity, dataset["frequency"] + ) + assert self._triple(g, dataset_ref, DCT.provenance, dataset["provenance"]) + assert self._triple(g, dataset_ref, DCT.type, dataset["dcat_type"]) + assert self._triple(g, dataset_ref, ADMS.versionNotes, dataset["version_notes"]) + assert self._triple(g, dataset_ref, DCT.accessRights, dataset["access_rights"]) + assert self._triple( + g, + dataset_ref, + DCAT.spatialResolutionInMeters, + dataset["spatial_resolution_in_meters"], + data_type=XSD.decimal, + ) + + # Dates + assert self._triple( + g, + dataset_ref, + DCT.issued, + dataset["issued"], + data_type=XSD.date, + ) + assert self._triple( + g, + dataset_ref, + DCT.modified, + dataset["modified"], + data_type=XSD.date, + ) + assert self._triple( + g, + dataset_ref, + DCAT.temporalResolution, + dataset["temporal_resolution"], + data_type=XSD.duration, + ) + + # List fields + + assert ( + self._triples_list_values(g, dataset_ref, DCT.conformsTo) + == dataset["conforms_to"] + ) + assert self._triples_list_values(g, dataset_ref, DCAT.theme) == dataset["theme"] + assert ( + self._triples_list_values(g, dataset_ref, DCT.language) + == dataset["language"] + ) + assert ( + self._triples_list_values(g, dataset_ref, FOAF.page) + == dataset["documentation"] + ) + assert ( + self._triples_list_values(g, dataset_ref, DCT.isReferencedBy) + == dataset["is_referenced_by"] + ) + assert ( + self._triples_list_values(g, dataset_ref, DCATAP.applicableLegislation) + == dataset["applicable_legislation"] + ) + + # Repeating subfields + + contact_details = [t for t in g.triples((dataset_ref, DCAT.contactPoint, None))] + + assert len(contact_details) == len(dataset["contact"]) + assert self._triple( + g, contact_details[0][2], VCARD.fn, dataset_dict["contact"][0]["name"] + ) + assert self._triple( + g, + contact_details[0][2], + VCARD.hasEmail, + URIRef("mailto:" + dataset_dict["contact"][0]["email"]), + ) + assert self._triple( + g, contact_details[1][2], VCARD.fn, dataset_dict["contact"][1]["name"] + ) + assert self._triple( + g, + contact_details[1][2], + VCARD.hasEmail, + URIRef("mailto:" + dataset_dict["contact"][1]["email"]), + ) + + publisher = [t for t in g.triples((dataset_ref, DCT.publisher, None))] + + assert len(publisher) == 1 + assert self._triple( + g, publisher[0][2], FOAF.name, dataset_dict["publisher"][0]["name"] + ) + assert self._triple( + g, + publisher[0][2], + VCARD.hasEmail, + URIRef("mailto:" + dataset_dict["publisher"][0]["email"]), + ) + assert self._triple( + g, + publisher[0][2], + FOAF.homepage, + URIRef(dataset_dict["publisher"][0]["url"]), + ) + assert self._triple( + g, + publisher[0][2], + DCT.type, + dataset_dict["publisher"][0]["type"], + ) + + temporal = [t for t in g.triples((dataset_ref, DCT.temporal, None))] + + assert len(temporal) == len(dataset["temporal_coverage"]) + assert self._triple( + g, + temporal[0][2], + DCAT.startDate, + dataset_dict["temporal_coverage"][0]["start"], + data_type=XSD.date, + ) + assert self._triple( + g, + temporal[0][2], + DCAT.endDate, + dataset_dict["temporal_coverage"][0]["end"], + data_type=XSD.date, + ) + assert self._triple( + g, + temporal[1][2], + DCAT.startDate, + dataset_dict["temporal_coverage"][1]["start"], + data_type=XSD.date, + ) + assert self._triple( + g, + temporal[1][2], + DCAT.endDate, + dataset_dict["temporal_coverage"][1]["end"], + data_type=XSD.date, + ) + + spatial = [t for t in g.triples((dataset_ref, DCT.spatial, None))] + assert len(spatial) == len(dataset["spatial_coverage"]) + assert str(spatial[0][2]) == dataset["spatial_coverage"][0]["uri"] + assert self._triple(g, spatial[0][2], RDF.type, DCT.Location) + assert self._triple( + g, spatial[0][2], SKOS.prefLabel, dataset["spatial_coverage"][0]["text"] + ) + + assert len([t for t in g.triples((spatial[0][2], LOCN.Geometry, None))]) == 1 + # Geometry in WKT + wkt_geom = wkt.dumps(dataset["spatial_coverage"][0]["geom"], decimals=4) + assert self._triple(g, spatial[0][2], LOCN.Geometry, wkt_geom, GSP.wktLiteral) + + distribution_ref = self._triple(g, dataset_ref, DCAT.distribution, None)[2] + resource = dataset_dict["resources"][0] + + # Alternate identifiers + ids = [] + for subject in [t[2] for t in g.triples((dataset_ref, ADMS.identifier, None))]: + ids.append(str(g.value(subject, SKOS.notation))) + assert ids == dataset["alternate_identifier"] + + # Resources: core fields + + assert self._triple(g, distribution_ref, DCT.title, resource["name"]) + assert self._triple( + g, + distribution_ref, + DCT.description, + resource["description"], + ) + + # Resources: standard fields + + assert self._triple(g, distribution_ref, DCT.rights, resource["rights"]) + assert self._triple( + g, distribution_ref, ADMS.status, URIRef(resource["status"]) + ) + assert self._triple( + g, + distribution_ref, + DCAT.accessURL, + URIRef(resource["access_url"]), + ) + assert self._triple( + g, + distribution_ref, + DCATAP.availability, + URIRef(resource["availability"]), + ) + assert self._triple( + g, + distribution_ref, + DCAT.compressFormat, + URIRef(resource["compress_format"]), + ) + assert self._triple( + g, + distribution_ref, + DCAT.packageFormat, + URIRef(resource["package_format"]), + ) + assert self._triple( + g, + distribution_ref, + DCAT.downloadURL, + URIRef(resource["download_url"]), + ) + + assert self._triple( + g, distribution_ref, DCAT.byteSize, resource["size"], XSD.nonNegativeInteger + ) + + # Checksum + checksum = self._triple(g, distribution_ref, SPDX.checksum, None)[2] + assert checksum + assert self._triple(g, checksum, RDF.type, SPDX.Checksum) + assert self._triple( + g, + checksum, + SPDX.checksumValue, + resource["hash"], + data_type="http://www.w3.org/2001/XMLSchema#hexBinary", + ) + assert self._triple( + g, checksum, SPDX.algorithm, URIRef(resource["hash_algorithm"]) + ) + + # Resources: dates + assert self._triple( + g, + distribution_ref, + DCT.issued, + dataset["resources"][0]["issued"], + data_type=XSD.dateTime, + ) + assert self._triple( + g, + distribution_ref, + DCT.modified, + dataset["resources"][0]["modified"], + data_type=XSD.dateTime, + ) + + # Resources: list fields + assert ( + self._triples_list_values(g, distribution_ref, DCT.language) + == resource["language"] + ) + + # Resource: repeating subfields + access_services = [ + t for t in g.triples((distribution_ref, DCAT.accessService, None)) + ] + + assert len(access_services) == len(dataset["resources"][0]["access_services"]) + assert self._triple( + g, + access_services[0][2], + DCT.title, + resource["access_services"][0]["title"], + ) + + endpoint_urls = [ + str(t[2]) + for t in g.triples((access_services[0][2], DCAT.endpointURL, None)) + ] + assert endpoint_urls == resource["access_services"][0]["endpoint_url"] + + def test_byte_size_non_negative_integer(self): + + dataset = { + "id": "4b6fe9ca-dc77-4cec-92a4-55c6624a5bd6", + "name": "test-dataset", + "title": "Test DCAT 2 dataset", + "notes": "Lorem ipsum", + "resources": [ + { + "id": "7fffe9b2-7a24-4d43-91f7-8bd58bad9615", + "url": "http://example.org/data.csv", + "size": 1234, + } + ], + } + + s = RDFSerializer(profiles=DCAT_AP_PROFILES) + g = s.g + + s.graph_from_dataset(dataset) + + triple = [t for t in g.triples((None, DCAT.byteSize, None))][0] + + assert triple[2].datatype == XSD.nonNegativeInteger + assert int(triple[2]) == 1234 diff --git a/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py b/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py deleted file mode 100644 index 62e58928..00000000 --- a/ckanext/dcat/tests/test_euro_dcatap_3_profile_serialize.py +++ /dev/null @@ -1,34 +0,0 @@ -from ckanext.dcat.profiles import DCAT, XSD -from ckanext.dcat.processors import RDFSerializer -from ckanext.dcat.tests.utils import BaseSerializeTest - -DCAT_AP_PROFILES = ["euro_dcat_ap_3"] - - -class TestEuroDCATAP2ProfileSerializeDataset(BaseSerializeTest): - - def test_byte_size_non_negative_integer(self): - - dataset = { - "id": "4b6fe9ca-dc77-4cec-92a4-55c6624a5bd6", - "name": "test-dataset", - "title": "Test DCAT 2 dataset", - "notes": "Lorem ipsum", - "resources": [ - { - "id": "7fffe9b2-7a24-4d43-91f7-8bd58bad9615", - "url": "http://example.org/data.csv", - "size": 1234, - } - ], - } - - s = RDFSerializer(profiles=DCAT_AP_PROFILES) - g = s.g - - dataset_ref = s.graph_from_dataset(dataset) - - triple = [t for t in g.triples((None, DCAT.byteSize, None))][0] - - assert triple[2].datatype == XSD.nonNegativeInteger - assert int(triple[2]) == 1234 From a77f32e3ab0f8505f89537712c5064789799dd6f Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 27 Aug 2024 17:02:23 +0200 Subject: [PATCH 13/14] Consolidate schema files as they are identical --- README.md | 4 +- ckanext/dcat/schemas/dcat_ap_3_full.yaml | 384 ------------------ ...cat_ap_2.1_full.yaml => dcat_ap_full.yaml} | 0 ...ommended.yaml => dcat_ap_recommended.yaml} | 0 .../dcat_ap_2/test_scheming_support.py | 8 +- .../test_euro_dcatap_3_profile_parse.py | 2 +- .../test_euro_dcatap_3_profile_serialize.py | 2 +- ckanext/dcat/tests/shacl/test_shacl.py | 8 +- 8 files changed, 12 insertions(+), 396 deletions(-) delete mode 100644 ckanext/dcat/schemas/dcat_ap_3_full.yaml rename ckanext/dcat/schemas/{dcat_ap_2.1_full.yaml => dcat_ap_full.yaml} (100%) rename ckanext/dcat/schemas/{dcat_ap_2.1_recommended.yaml => dcat_ap_recommended.yaml} (100%) diff --git a/README.md b/README.md index cd1b51e1..83c8baa6 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ The extension includes ready to use [ckanext-scheming](https://github.com/ckan/c There are the following schemas currently included with the extension: -* *dcat_ap_2.1_recommended.yaml*: Includes the recommended properties for `dcat:Dataset` and `dcat:Distribution` according to the [DCAT 2.1](https://semiceu.github.io/DCAT-AP/releases/2.1.1/) specification. -* *dcat_ap_2.1_full.yaml*: Includes most of the properties defined for `dcat:Dataset` and `dcat:Distribution` in the [DCAT 2.1](https://semiceu.github.io/DCAT-AP/releases/2.1.1/) specification. +* *dcat_ap_recommended.yaml*: Includes the recommended properties for `dcat:Dataset` and `dcat:Distribution` according to the DCAT AP specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_scheming`) and `euro_dcat_ap_3` profiles. +* *dcat_ap_full.yaml*: Includes most of the properties defined for `dcat:Dataset` and `dcat:Distribution` in the [DCAT AP 2.1](https://semiceu.github.io/DCAT-AP/releases/2.1.1/) and [DCAT AP v3](https://semiceu.github.io/DCAT-AP/releases/3.0.0/) specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_scheming`) and `euro_dcat_ap_3` profiles. Most sites will want to use these as a base to create their own custom schema to address their own requirements, perhaps alongside a [custom profile](#writing-custom-profiles). Of course site maintainers can add or remove schema fields, as well as change the existing validators. diff --git a/ckanext/dcat/schemas/dcat_ap_3_full.yaml b/ckanext/dcat/schemas/dcat_ap_3_full.yaml deleted file mode 100644 index 8f9f4afc..00000000 --- a/ckanext/dcat/schemas/dcat_ap_3_full.yaml +++ /dev/null @@ -1,384 +0,0 @@ -scheming_version: 2 -dataset_type: dataset -about: Full DCAT AP 2.1 schema -about_url: http://github.com/ckan/ckanext-dcat - -dataset_fields: - -- field_name: title - label: Title - preset: title - required: true - help_text: A descriptive title for the dataset. - -- field_name: name - label: URL - preset: dataset_slug - form_placeholder: eg. my-dataset - -- field_name: notes - label: Description - required: true - form_snippet: markdown.html - help_text: A free-text account of the dataset. - -- field_name: tag_string - label: Keywords - preset: tag_string_autocomplete - form_placeholder: eg. economy, mental health, government - help_text: Keywords or tags describing the dataset. Use commas to separate multiple values. - -- field_name: contact - label: Contact points - repeating_label: Contact point - repeating_subfields: - - - field_name: uri - label: URI - - - field_name: name - label: Name - - - field_name: email - label: Email - display_snippet: email.html - help_text: Contact information for enquiries about the dataset. - -- field_name: publisher - label: Publisher - repeating_label: Publisher - repeating_once: true - repeating_subfields: - - - field_name: uri - label: URI - - - field_name: name - label: Name - - - field_name: email - label: Email - display_snippet: email.html - - - field_name: url - label: URL - display_snippet: link.html - - - field_name: type - label: Type - help_text: Entity responsible for making the dataset available. - -- field_name: license_id - label: License - form_snippet: license.html - help_text: License definitions and additional information can be found at http://opendefinition.org/. - -- field_name: owner_org - label: Organization - preset: dataset_organization - help_text: The CKAN organization the dataset belongs to. - -- field_name: url - label: Landing page - form_placeholder: http://example.com/dataset.json - display_snippet: link.html - help_text: Web page that can be navigated to gain access to the dataset, its distributions and/or additional information. - - # Note: this will fall back to metadata_created if not present -- field_name: issued - label: Release date - preset: dcat_date - help_text: Date of publication of the dataset. - - # Note: this will fall back to metadata_modified if not present -- field_name: modified - label: Modification date - preset: dcat_date - help_text: Most recent date on which the dataset was changed, updated or modified. - -- field_name: version - label: Version - validators: ignore_missing unicode_safe package_version_validator - help_text: Version number or other version designation of the dataset. - -- field_name: version_notes - label: Version notes - validators: ignore_missing unicode_safe - form_snippet: markdown.html - display_snippet: markdown.html - help_text: A description of the differences between this version and a previous version of the dataset. - - # Note: CKAN will generate a unique identifier for each dataset -- field_name: identifier - label: Identifier - help_text: A unique identifier of the dataset. - -- field_name: frequency - label: Frequency - help_text: The frequency at which dataset is published. - -- field_name: provenance - label: Provenance - form_snippet: markdown.html - display_snippet: markdown.html - help_text: A statement about the lineage of the dataset. - -- field_name: dcat_type - label: Type - help_text: The type of the dataset. - # TODO: controlled vocabulary? - -- field_name: temporal_coverage - label: Temporal coverage - repeating_subfields: - - - field_name: start - label: Start - preset: dcat_date - - - field_name: end - label: End - preset: dcat_date - help_text: The temporal period or periods the dataset covers. - -- field_name: temporal_resolution - label: Temporal resolution - help_text: Minimum time period resolvable in the dataset. - -- field_name: spatial_coverage - label: Spatial coverage - repeating_subfields: - - - field_name: uri - label: URI - - - field_name: text - label: Label - - - field_name: geom - label: Geometry - - - field_name: bbox - label: Bounding Box - - - field_name: centroid - label: Centroid - help_text: A geographic region that is covered by the dataset. - -- field_name: spatial_resolution_in_meters - label: Spatial resolution in meters - help_text: Minimum spatial separation resolvable in a dataset, measured in meters. - -- field_name: access_rights - label: Access rights - validators: ignore_missing unicode_safe - form_snippet: markdown.html - display_snippet: markdown.html - help_text: Information that indicates whether the dataset is Open Data, has access restrictions or is not public. - -- field_name: alternate_identifier - label: Other identifier - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: This property refers to a secondary identifier of the dataset, such as MAST/ADS, DataCite, DOI, etc. - -- field_name: theme - label: Theme - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: A category of the dataset. A Dataset may be associated with multiple themes. - -- field_name: language - label: Language - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: Language or languages of the dataset. - # TODO: language form snippet / validator / graph - -- field_name: documentation - label: Documentation - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: A page or document about this dataset. - -- field_name: conforms_to - label: Conforms to - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: An implementing rule or other specification that the dataset follows. - -- field_name: is_referenced_by - label: Is referenced by - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: A related resource, such as a publication, that references, cites, or otherwise points to the dataset. - -- field_name: applicable_legislation - label: Applicable legislation - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: The legislation that mandates the creation or management of the dataset. - -#- field_name: hvd_category -# label: HVD Category -# preset: multiple_text -# validators: ignore_missing scheming_multiple_text -# TODO: implement separately as part of wider HVD support - -# Note: if not provided, this will be autogenerated -- field_name: uri - label: URI - help_text: An URI for this dataset (if not provided it will be autogenerated). - -# TODO: relation-based properties are not yet included (e.g. is_version_of, source, sample, etc) -# -resource_fields: - -- field_name: url - label: URL - preset: resource_url_upload - -- field_name: name - label: Name - form_placeholder: - help_text: A descriptive title for the resource. - -- field_name: description - label: Description - form_snippet: markdown.html - help_text: A free-text account of the resource. - -- field_name: format - label: Format - preset: resource_format_autocomplete - help_text: File format. If not provided it will be guessed. - -- field_name: mimetype - label: Media type - validators: if_empty_guess_format ignore_missing unicode_safe - help_text: Media type for this format. If not provided it will be guessed. - -- field_name: compress_format - label: Compress format - help_text: The format of the file in which the data is contained in a compressed form. - -- field_name: package_format - label: Package format - help_text: The format of the file in which one or more data files are grouped together. - -- field_name: size - label: Size - validators: ignore_missing int_validator - form_snippet: number.html - display_snippet: file_size.html - help_text: File size in bytes - -- field_name: hash - label: Hash - help_text: Checksum of the downloaded file. - -- field_name: hash_algorithm - label: Hash Algorithm - help_text: Algorithm used to calculate to checksum. - -- field_name: rights - label: Rights - form_snippet: markdown.html - display_snippet: markdown.html - help_text: Some statement about the rights associated with the resource. - -- field_name: availability - label: Availability - help_text: Indicates how long it is planned to keep the resource available. - -- field_name: status - label: Status - preset: select - choices: - - value: http://purl.org/adms/status/Completed - label: Completed - - value: http://purl.org/adms/status/UnderDevelopment - label: Under Development - - value: http://purl.org/adms/status/Deprecated - label: Deprecated - - value: http://purl.org/adms/status/Withdrawn - label: Withdrawn - help_text: The status of the resource in the context of maturity lifecycle. - -- field_name: license - label: License - help_text: License in which the resource is made available. If not provided will be inherited from the dataset. - - # Note: this falls back to the standard resource url field -- field_name: access_url - label: Access URL - help_text: URL that gives access to the dataset (defaults to the standard resource URL). - - # Note: this falls back to the standard resource url field -- field_name: download_url - label: Download URL - help_text: URL that provides a direct link to a downloadable file (defaults to the standard resource URL). - -- field_name: issued - label: Release date - preset: dcat_date - help_text: Date of publication of the resource. - -- field_name: modified - label: Modification date - preset: dcat_date - help_text: Most recent date on which the resource was changed, updated or modified. - -- field_name: language - label: Language - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: Language or languages of the resource. - -- field_name: documentation - label: Documentation - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: A page or document about this resource. - -- field_name: conforms_to - label: Conforms to - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: An established schema to which the described resource conforms. - -- field_name: applicable_legislation - label: Applicable legislation - preset: multiple_text - validators: ignore_missing scheming_multiple_text - help_text: The legislation that mandates the creation or management of the resource. - -- field_name: access_services - label: Access services - repeating_label: Access service - repeating_subfields: - - - field_name: uri - label: URI - - - field_name: title - label: Title - - - field_name: endpoint_description - label: Endpoint description - - - field_name: endpoint_url - label: Endpoint URL - preset: multiple_text - - - field_name: serves_dataset - label: Serves dataset - preset: multiple_text - validators: ignore_missing scheming_multiple_text - - help_text: A data service that gives access to the resource. - - # Note: if not provided, this will be autogenerated -- field_name: uri - label: URI - help_text: An URI for this resource (if not provided it will be autogenerated). diff --git a/ckanext/dcat/schemas/dcat_ap_2.1_full.yaml b/ckanext/dcat/schemas/dcat_ap_full.yaml similarity index 100% rename from ckanext/dcat/schemas/dcat_ap_2.1_full.yaml rename to ckanext/dcat/schemas/dcat_ap_full.yaml diff --git a/ckanext/dcat/schemas/dcat_ap_2.1_recommended.yaml b/ckanext/dcat/schemas/dcat_ap_recommended.yaml similarity index 100% rename from ckanext/dcat/schemas/dcat_ap_2.1_recommended.yaml rename to ckanext/dcat/schemas/dcat_ap_recommended.yaml diff --git a/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py b/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py index de8a8a88..bec6d911 100644 --- a/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py +++ b/ckanext/dcat/tests/profiles/dcat_ap_2/test_scheming_support.py @@ -32,7 +32,7 @@ @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -673,7 +673,7 @@ def test_dcat_date(self): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -705,7 +705,7 @@ def test_mimetype_is_guessed(self): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -860,7 +860,7 @@ def test_e2e_dcat_to_ckan(self): @pytest.mark.usefixtures("with_plugins", "clean_db", "clean_index") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", diff --git a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py index 286a9692..e887a24d 100644 --- a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py +++ b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py @@ -9,7 +9,7 @@ @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", diff --git a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py index 382d6c76..e0001526 100644 --- a/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py +++ b/ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py @@ -32,7 +32,7 @@ class TestEuroDCATAP3ProfileSerializeDataset(BaseSerializeTest): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", diff --git a/ckanext/dcat/tests/shacl/test_shacl.py b/ckanext/dcat/tests/shacl/test_shacl.py index af027491..7dbd8a5b 100644 --- a/ckanext/dcat/tests/shacl/test_shacl.py +++ b/ckanext/dcat/tests/shacl/test_shacl.py @@ -49,7 +49,7 @@ def _results_count(results_graph): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -73,7 +73,7 @@ def test_validate_dcat_ap_2_graph_shapes(): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -127,7 +127,7 @@ def test_validate_dcat_ap_2_legacy_graph_shapes_recommended(): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_2.1_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", @@ -167,7 +167,7 @@ def test_validate_dcat_ap_2_graph_shapes_range(): @pytest.mark.usefixtures("with_plugins", "clean_db") @pytest.mark.ckan_config("ckan.plugins", "dcat scheming_datasets") @pytest.mark.ckan_config( - "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_3_full.yaml" + "scheming.dataset_schemas", "ckanext.dcat.schemas:dcat_ap_full.yaml" ) @pytest.mark.ckan_config( "scheming.presets", From 4ae879571265a37d4f397d3af0be750c212df2d3 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 28 Aug 2024 15:14:42 +0200 Subject: [PATCH 14/14] Default to euro_dcat_ap_3 profile Fix method call in dcat ap 3 profile, surfaced after switching the default profile. Add changelog and update docs. --- CHANGELOG.md | 4 ++++ README.md | 15 ++++++++------- ckanext/dcat/processors.py | 2 +- ckanext/dcat/profiles/euro_dcat_ap_3.py | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69aea84f..ea2c482f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased](https://github.com/ckan/ckanext-dcat/compare/v1.7.0...HEAD) +* New profile for [DCAT-AP v3](https://semiceu.github.io/DCAT-AP/releases/3.0.0), `euro_dcat_ap_3`, which is now + the default. Existing sites willing to stick with DCAT-AP v2.x can specify the profile in the configuration if they + are not doing so yet (`ckan.dcat.rdf.profiles = euro_dcat_ap_2`). The new `euro_dcat_ap_3` profile relies on + ckanext-scheming metadata schemas (see below). * Support for standard CKAN [ckanext-scheming](https://github.com/ckan/ckanext-scheming) schemas. The DCAT profiles now seamlessly integrate with fields defined via the YAML or JSON scheming files. Sites willing to migrate to a scheming based metadata schema can do diff --git a/README.md b/README.md index 83c8baa6..1a11c697 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ The extension includes ready to use [ckanext-scheming](https://github.com/ckan/c There are the following schemas currently included with the extension: -* *dcat_ap_recommended.yaml*: Includes the recommended properties for `dcat:Dataset` and `dcat:Distribution` according to the DCAT AP specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_scheming`) and `euro_dcat_ap_3` profiles. -* *dcat_ap_full.yaml*: Includes most of the properties defined for `dcat:Dataset` and `dcat:Distribution` in the [DCAT AP 2.1](https://semiceu.github.io/DCAT-AP/releases/2.1.1/) and [DCAT AP v3](https://semiceu.github.io/DCAT-AP/releases/3.0.0/) specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_scheming`) and `euro_dcat_ap_3` profiles. +* *dcat_ap_recommended.yaml*: Includes the recommended properties for `dcat:Dataset` and `dcat:Distribution` according to the DCAT AP specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_ap_scheming`) and `euro_dcat_ap_3` profiles. +* *dcat_ap_full.yaml*: Includes most of the properties defined for `dcat:Dataset` and `dcat:Distribution` in the [DCAT AP 2.1](https://semiceu.github.io/DCAT-AP/releases/2.1.1/) and [DCAT AP v3](https://semiceu.github.io/DCAT-AP/releases/3.0.0/) specification. You can use this schema with the `euro_dcat_ap_2` (+ `euro_dcat_ap_scheming`) and `euro_dcat_ap_3` profiles. Most sites will want to use these as a base to create their own custom schema to address their own requirements, perhaps alongside a [custom profile](#writing-custom-profiles). Of course site maintainers can add or remove schema fields, as well as change the existing validators. @@ -864,13 +864,14 @@ They essentially define the mapping between DCAT and CKAN. In most cases the default profile will provide a good mapping that will cover most properties described in the DCAT standard. If you want to extract extra fields defined in the RDF, are using a custom schema or need custom logic, you can write a custom to profile that extends or replaces the default one. -The default profile is mostly based in the -[DCAT application profile for data portals in Europe](https://joinup.ec.europa.eu/asset/dcat_application_profile/description). It is actually fully-compatible with [DCAT-AP v1.1](https://joinup.ec.europa.eu/asset/dcat_application_profile/asset_release/dcat-ap-v11), and partially compatible with [DCAT-AP v2.1.0](https://joinup.ec.europa.eu/collection/semantic-interoperability-community-semic/solution/dcat-application-profile-data-portals-europe/release/210). As mentioned before though, it should be generic enough for most DCAT based representations. +The profiles currently shipped with the extension are mostly based in the +[DCAT application profile for data portals in Europe](https://joinup.ec.europa.eu/asset/dcat_application_profile/description). As mentioned before though, they should be generic enough for most DCAT based representations. -Sites that want to support a particular version of the DCAT-AP can enable a specific profile using one of the methods below: +Sites that want to support a particular version of the DCAT-AP can enable a specific profile using one of the profiles below: -* DCAT-AP v2.1.0 (default): `euro_dcat_ap_2` -* DCAT-AP v1.1.1: `euro_dcat_ap` +* [DCAT-AP v3](https://semiceu.github.io/DCAT-AP/releases/3.0.0) (default): `euro_dcat_ap_3` +* [DCAT-AP v2.1.0](https://joinup.ec.europa.eu/collection/semantic-interoperability-community-semic/solution/dcat-application-profile-data-portals-europe/release/210): `euro_dcat_ap_2` +* [DCAT-AP v1.1.1](https://joinup.ec.europa.eu/asset/dcat_application_profile/asset_release/dcat-ap-v11): `euro_dcat_ap` This plugin also contains a profile to serialize a CKAN dataset to a [schema.org Dataset](http://schema.org/Dataset) called `schemaorg`. This is especially useful to provide [JSON-LD structured data](#structured-data). diff --git a/ckanext/dcat/processors.py b/ckanext/dcat/processors.py index 1254e86d..ef3230f6 100644 --- a/ckanext/dcat/processors.py +++ b/ckanext/dcat/processors.py @@ -28,7 +28,7 @@ RDF_PROFILES_CONFIG_OPTION = 'ckanext.dcat.rdf.profiles' COMPAT_MODE_CONFIG_OPTION = 'ckanext.dcat.compatibility_mode' -DEFAULT_RDF_PROFILES = ['euro_dcat_ap_2'] +DEFAULT_RDF_PROFILES = ['euro_dcat_ap_3'] def _get_default_rdf_profiles(): diff --git a/ckanext/dcat/profiles/euro_dcat_ap_3.py b/ckanext/dcat/profiles/euro_dcat_ap_3.py index d0cce700..d2dfbaed 100644 --- a/ckanext/dcat/profiles/euro_dcat_ap_3.py +++ b/ckanext/dcat/profiles/euro_dcat_ap_3.py @@ -46,7 +46,7 @@ def graph_from_dataset(self, dataset_dict, dataset_ref): def graph_from_catalog(self, catalog_dict, catalog_ref): - self._graph_from_catalog_base(self, catalog_dict, catalog_ref) + self._graph_from_catalog_base(catalog_dict, catalog_ref) def _graph_from_dataset_v3(self, dataset_dict, dataset_ref):