Skip to content

Commit 80290d0

Browse files
committed
Merge branch 'master' of github.com:ckan/ckanext-dcat
2 parents be3f8d3 + bbbe1a5 commit 80290d0

7 files changed

Lines changed: 166 additions & 18 deletions

File tree

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ jobs:
1212
name: Build distribution
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v4
15+
- uses: actions/checkout@v5
1616
- name: Set up Python
17-
uses: actions/setup-python@v5
17+
uses: actions/setup-python@v6
1818
with:
1919
python-version: "3.9"
2020
- name: Install pypa/build

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ jobs:
66
lint:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v4
10-
- uses: actions/setup-python@v5
9+
- uses: actions/checkout@v5
10+
- uses: actions/setup-python@v6
1111
with:
1212
python-version: '3.9'
1313
- name: Install requirements
@@ -53,7 +53,7 @@ jobs:
5353
CKAN_REDIS_URL: redis://redis:6379/1
5454

5555
steps:
56-
- uses: actions/checkout@v4
56+
- uses: actions/checkout@v5
5757
- name: Install requirements (common)
5858
run: |
5959
pip install -r requirements.txt
@@ -77,4 +77,4 @@ jobs:
7777
ckan -c test.ini db init
7878
ckan -c test.ini db pending-migrations --apply
7979
- name: Run tests
80-
run: pytest --ckan-ini=test.ini --cov=ckanext.dcat --cov-report=term-missing --cov-append --disable-warnings ckanext/dcat/tests
80+
run: pytest --ckan-ini=test.ini --cov=ckanext.dcat --cov-report=term-missing --cov-append --disable-warnings ckanext/dcat/tests

ckanext/dcat/profiles/euro_dcat_ap_3.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
SKOS,
77
ADMS,
88
RDF,
9+
OWL,
910
)
1011

1112
from .base import URIRefOrLiteral
@@ -31,6 +32,18 @@ def parse_dataset(self, dataset_dict, dataset_ref):
3132
dataset_dict = self._parse_dataset_v2_scheming(dataset_dict, dataset_ref)
3233

3334
# DCAT AP v3: hasVersion
35+
dataset_dict = self._parse_dataset_v3(dataset_dict, dataset_ref)
36+
37+
return dataset_dict
38+
39+
def _parse_dataset_v3(self, dataset_dict, dataset_ref):
40+
41+
# version
42+
value = self._object_value(dataset_ref, DCAT.version)
43+
if value:
44+
dataset_dict["version"] = value
45+
46+
# hasVersion
3447
values = self._object_value_list(dataset_ref, DCAT.hasVersion)
3548
if values:
3649
dataset_dict["has_version"] = values
@@ -51,12 +64,6 @@ def graph_from_dataset(self, dataset_dict, dataset_ref):
5164
# DCAT AP v3 properties also applied to higher versions
5265
self._graph_from_dataset_v3(dataset_dict, dataset_ref)
5366

54-
# DCAT AP v3: List triples
55-
items = [
56-
("has_version", DCAT.hasVersion, None, URIRefOrLiteral),
57-
]
58-
self._add_list_triples_from_dict(dataset_dict, dataset_ref, items)
59-
6067
def graph_from_catalog(self, catalog_dict, catalog_ref):
6168

6269
self._graph_from_catalog_base(catalog_dict, catalog_ref)
@@ -71,6 +78,22 @@ def _graph_from_dataset_v3(self, dataset_dict, dataset_ref):
7178
self.g.remove((dataset_ref, RDF.type, None))
7279
self.g.add((dataset_ref, RDF.type, DCAT.DatasetSeries))
7380

81+
# version own:versionInfo -> dcat:version
82+
self.g.remove((dataset_ref, OWL.versionInfo, None))
83+
self._add_triple_from_dict(
84+
dataset_dict,
85+
dataset_ref,
86+
DCAT.version,
87+
"version",
88+
_type=Literal,
89+
)
90+
91+
# hasVersion
92+
items = [
93+
("has_version", DCAT.hasVersion, None, URIRefOrLiteral),
94+
]
95+
self._add_list_triples_from_dict(dataset_dict, dataset_ref, items)
96+
7497
# byteSize decimal -> nonNegativeInteger
7598
for subject, predicate, object in self.g.triples((None, DCAT.byteSize, None)):
7699
if object and object.datatype == XSD.decimal:

ckanext/dcat/profiles/euro_dcat_ap_scheming.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
SKOS,
1212
LOCN,
1313
RDFS,
14+
SCHEMA,
15+
TIME,
1416
)
1517

1618

@@ -30,6 +32,47 @@ def graph_from_dataset(self, dataset_dict, dataset_ref):
3032

3133
self._graph_from_dataset_v2_scheming(dataset_dict, dataset_ref)
3234

35+
def _temporal_interval_details(self, interval_ref):
36+
details = {}
37+
38+
start = self._object_value(interval_ref, DCAT.startDate)
39+
end = self._object_value(interval_ref, DCAT.endDate)
40+
41+
if not (start or end):
42+
start_nodes = [t for t in self.g.objects(interval_ref, TIME.hasBeginning)]
43+
end_nodes = [t for t in self.g.objects(interval_ref, TIME.hasEnd)]
44+
if start_nodes:
45+
start = self._object_value_multiple_predicate(
46+
start_nodes[0],
47+
[TIME.inXSDDateTimeStamp, TIME.inXSDDateTime, TIME.inXSDDate],
48+
)
49+
if end_nodes:
50+
end = self._object_value_multiple_predicate(
51+
end_nodes[0],
52+
[TIME.inXSDDateTimeStamp, TIME.inXSDDateTime, TIME.inXSDDate],
53+
)
54+
55+
if not (start or end):
56+
start = self._object_value(interval_ref, SCHEMA.startDate)
57+
end = self._object_value(interval_ref, SCHEMA.endDate)
58+
59+
if start:
60+
details["start"] = start
61+
if end:
62+
details["end"] = end
63+
64+
return details
65+
66+
def _temporal_coverage_details(self, dataset_ref):
67+
temporal_coverage = []
68+
69+
for interval_ref in self.g.objects(dataset_ref, DCT.temporal):
70+
details = self._temporal_interval_details(interval_ref)
71+
if details:
72+
temporal_coverage.append(details)
73+
74+
return temporal_coverage
75+
3376
def _parse_dataset_v2_scheming(self, dataset_dict, dataset_ref):
3477
"""
3578
Modify the dataset_dict generated by the euro_dcat_ap and euro_dcat_ap_2
@@ -112,12 +155,14 @@ def _prune_agent_translations(agent_list):
112155
new_extras = []
113156
new_dict = {}
114157
check_name = new_fields_mapping.get(field_name, field_name)
158+
subfield_names = [
159+
subfield["field_name"]
160+
for subfield in schema_field["repeating_subfields"]
161+
]
115162
for extra in dataset_dict.get("extras", []):
116163
if extra["key"].startswith(f"{check_name}_"):
117164
subfield = extra["key"][extra["key"].index("_") + 1 :]
118-
if subfield in [
119-
f["field_name"] for f in schema_field["repeating_subfields"]
120-
]:
165+
if subfield in subfield_names:
121166
new_dict[subfield] = extra["value"]
122167
else:
123168
new_extras.append(extra)
@@ -126,8 +171,22 @@ def _prune_agent_translations(agent_list):
126171
new_dict["geom"] = extra["value"]
127172
else:
128173
new_extras.append(extra)
174+
# Legacy profiles may have already promoted namespaced extras such as
175+
# `temporal_start` to first-level schema fields before we get here.
176+
# Rebuild the repeating structure from those root fields as well.
177+
for subfield in subfield_names:
178+
legacy_field_name = f"{check_name}_{subfield}"
179+
value = dataset_dict.get(legacy_field_name)
180+
if value not in (None, "", [], {}):
181+
new_dict.setdefault(subfield, value)
182+
if field_name == "temporal_coverage":
183+
temporal_coverage = self._temporal_coverage_details(dataset_ref)
184+
if temporal_coverage:
185+
dataset_dict[field_name] = temporal_coverage
186+
dataset_dict["extras"] = new_extras
187+
continue
129188
if new_dict:
130-
dataset_dict[field_name] = [new_dict]
189+
dataset_dict.setdefault(field_name, [new_dict])
131190
dataset_dict["extras"] = new_extras
132191

133192
# Contact details

ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_parse.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import pytest
2+
from rdflib import BNode, Graph, Literal, URIRef
3+
from rdflib.namespace import RDF
24

35
from ckan.tests.helpers import call_action
46

57
from ckanext.dcat.processors import RDFParser
8+
from ckanext.dcat.profiles import DCAT, DCT, XSD
69
from ckanext.dcat.tests.utils import BaseParseTest
710

811

@@ -160,3 +163,66 @@ def test_e2e_dcat_to_ckan(self):
160163
assert resource["access_services"][0]["endpoint_url"] == [
161164
"http://publications.europa.eu/webapi/rdf/sparql"
162165
]
166+
167+
def test_multiple_temporal_periods_are_parsed_into_temporal_coverage(self):
168+
g = Graph()
169+
170+
dataset_ref = URIRef("http://example.com/datasets/temporal-multi")
171+
first_period = BNode()
172+
second_period = BNode()
173+
174+
g.add((dataset_ref, RDF.type, DCAT.Dataset))
175+
g.add((dataset_ref, DCT.title, Literal("Dataset with multiple periods")))
176+
g.add((dataset_ref, DCT.description, Literal("Test dataset")))
177+
g.add((dataset_ref, DCT.temporal, first_period))
178+
g.add((dataset_ref, DCT.temporal, second_period))
179+
g.add(
180+
(
181+
first_period,
182+
DCAT.startDate,
183+
Literal("2021-01-01T00:00:00Z", datatype=XSD.dateTime),
184+
)
185+
)
186+
g.add(
187+
(
188+
first_period,
189+
DCAT.endDate,
190+
Literal("2021-12-31T00:00:00Z", datatype=XSD.dateTime),
191+
)
192+
)
193+
g.add(
194+
(
195+
second_period,
196+
DCAT.startDate,
197+
Literal("2022-01-01T00:00:00Z", datatype=XSD.dateTime),
198+
)
199+
)
200+
g.add(
201+
(
202+
second_period,
203+
DCAT.endDate,
204+
Literal("2022-12-31T00:00:00Z", datatype=XSD.dateTime),
205+
)
206+
)
207+
208+
p = RDFParser()
209+
p.g = g
210+
211+
datasets = [d for d in p.datasets()]
212+
213+
assert len(datasets) == 1
214+
215+
temporal_coverage = sorted(
216+
datasets[0]["temporal_coverage"], key=lambda item: item["start"]
217+
)
218+
219+
assert temporal_coverage == [
220+
{
221+
"start": "2021-01-01T00:00:00+00:00",
222+
"end": "2021-12-31T00:00:00+00:00",
223+
},
224+
{
225+
"start": "2022-01-01T00:00:00+00:00",
226+
"end": "2022-12-31T00:00:00+00:00",
227+
},
228+
]

ckanext/dcat/tests/profiles/dcat_ap_3/test_euro_dcatap_3_profile_serialize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_e2e_ckan_to_dcat(self):
6868
assert self._triple(g, dataset_ref, RDF.type, DCAT.Dataset)
6969
assert self._triple(g, dataset_ref, DCT.title, dataset["title"])
7070
assert self._triple(g, dataset_ref, DCT.description, dataset["notes"])
71-
assert self._triple(g, dataset_ref, OWL.versionInfo, dataset["version"])
71+
assert self._triple(g, dataset_ref, DCAT.version, dataset["version"])
7272

7373
# Standard fields
7474
assert self._triple(g, dataset_ref, DCT.identifier, dataset["identifier"])

ckanext/dcat/tests/profiles/dcat_us_3/test_dcat_us_3_profile_serialize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_e2e_ckan_to_dcat(self):
6969
assert self._triple(g, dataset_ref, RDF.type, DCAT.Dataset)
7070
assert self._triple(g, dataset_ref, DCT.title, dataset["title"])
7171
assert self._triple(g, dataset_ref, DCT.description, dataset["notes"])
72-
assert self._triple(g, dataset_ref, OWL.versionInfo, dataset["version"])
72+
assert self._triple(g, dataset_ref, DCAT.version, dataset["version"])
7373

7474
# Standard fields
7575
assert self._triple(g, dataset_ref, DCT.identifier, dataset["identifier"])

0 commit comments

Comments
 (0)