From f6d9d4b76d944512a6e952d28af16800feae1449 Mon Sep 17 00:00:00 2001 From: streino Date: Mon, 6 Oct 2025 16:07:03 +0200 Subject: [PATCH 01/11] feat: Add remote URL prefix option to csw-dcat harvester --- udata/harvest/backends/dcat.py | 18 +++---- udata/harvest/tests/test_dcat_backend.py | 62 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py index 1f61b9d45d..f1cce34329 100644 --- a/udata/harvest/backends/dcat.py +++ b/udata/harvest/backends/dcat.py @@ -258,6 +258,15 @@ class CswDcatBackend(DcatBackend): display_name = "CSW-DCAT" + extra_configs = ( + HarvestExtraConfig( + _("Remote URL prefix"), + "remote_url_prefix", + str, + _("A prefix used to build the remote URL of the harvested items."), + ), + ) + # CSW_REQUEST is based on: # - Request syntax from spec [1] and example requests [1] [2]. # - Sort settings to ensure stable paging [3]. @@ -426,15 +435,6 @@ class CswIso19139DcatBackend(CswDcatBackend): display_name = "CSW-ISO-19139" - extra_configs = ( - HarvestExtraConfig( - _("Remote URL prefix"), - "remote_url_prefix", - str, - _("A prefix used to build the remote URL of the harvested items."), - ), - ) - CSW_OUTPUT_SCHEMA = "http://www.isotc211.org/2005/gmd" def __init__(self, *args, **kwargs): diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index a19ec9761f..b2ec63415c 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -1021,6 +1021,68 @@ def test_disallow_external_dtd(self, rmock): assert job.status == "done" assert len(job.items) == 1 + @pytest.mark.parametrize( + "remote_url_prefix", + [ + None, + "http://catalog.example.com", # no trailing slash + "http://catalog.example.com/", # trailing slash + ], + ) + def test_url_prefix(self, rmock, remote_url_prefix: str): + xml = """ + + + + + + + id-1 + + + dataset-1 + Dataset 1 + + + + + + """ + rmock.get("http://data.example.com/datasets/dataset-1", status_code=200) + rmock.head(rmock.ANY, headers={"Content-Type": "application/xml"}) + rmock.post(rmock.ANY, text=xml) + + source = HarvestSourceFactory( + backend="csw-dcat", + config={ + "extra_configs": [ + { + "key": "remote_url_prefix", + "value": remote_url_prefix, + } + ] + }, + ) + + actions.run(source) + source.reload() + job = source.get_last_job() + assert len(job.items) == 1 + + dataset = Dataset.objects[0] + if remote_url_prefix: + # Computed from source config `remote_url_prefix` + metadata `dct:identifier`. + assert dataset.harvest.remote_url == "http://catalog.example.com/id-1" + else: + # First `dct:landingPage` found in the resource. + # If it breaks, it's not necessarily a bug — this acts as a demonstration of current behavior. + assert dataset.harvest.remote_url == "http://data.example.com/datasets/dataset-1" + @pytest.mark.usefixtures("clean_db") @pytest.mark.options(PLUGINS=["csw"]) From 9af9b9f64364101477deca198138a43fa249b8e2 Mon Sep 17 00:00:00 2001 From: streino Date: Fri, 12 Dec 2025 18:21:27 +0100 Subject: [PATCH 02/11] feat(harvest): add GeoDCAT-AP option for csw-dcat harvesters --- udata/harvest/backends/dcat.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py index efab3f1d07..a8c4feeb68 100644 --- a/udata/harvest/backends/dcat.py +++ b/udata/harvest/backends/dcat.py @@ -15,6 +15,7 @@ from udata.rdf import ( DCAT, DCT, + GEODCAT, HYDRA, SPDX, guess_format, @@ -261,6 +262,12 @@ class CswDcatBackend(DcatBackend): display_name = "CSW-DCAT" extra_configs = ( + HarvestExtraConfig( + _("GeoDCAT-AP"), + "enable_geodcat", + str, + _("Request GeoDCAT-AP to the CSW server (must be supported by the server)."), + ), HarvestExtraConfig( _("Remote URL prefix"), "remote_url_prefix", @@ -333,8 +340,6 @@ class CswDcatBackend(DcatBackend): """ - CSW_OUTPUT_SCHEMA = "http://www.w3.org/ns/dcat#" - SAXON_SECURITY_FEATURES = { "http://saxon.sf.net/feature/allow-external-functions": "false", "http://saxon.sf.net/feature/parserFeature?uri=http://apache.org/xml/features/nonvalidating/load-external-dtd": "false", @@ -353,15 +358,23 @@ def __init__(self, *args, **kwargs): self.xpath_proc = self.saxon_proc.new_xpath_processor() self.xpath_proc.declare_namespace("csw", CSW_NAMESPACE) + @property + def output_schema(self): + if self.get_extra_config_value("enable_geodcat"): + return str(GEODCAT) + else: + return str(DCAT) + def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]: """ Yield all RDF pages as `Graph` from the source """ + output_schema = self.output_schema page_number = 0 start = 1 while True: - data = self.CSW_REQUEST.format(output_schema=self.CSW_OUTPUT_SCHEMA, start=start) + data = self.CSW_REQUEST.format(output_schema=output_schema, start=start) response = self.post(url, data=data, headers={"Content-Type": "application/xml"}) response.raise_for_status() @@ -438,7 +451,7 @@ class CswIso19139DcatBackend(CswDcatBackend): name = "csw-iso-19139" display_name = "CSW-ISO-19139" - CSW_OUTPUT_SCHEMA = "http://www.isotc211.org/2005/gmd" + extra_configs = [c for c in CswDcatBackend.extra_configs if c.key != "enable_geodcat"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -450,6 +463,11 @@ def __init__(self, *args, **kwargs): "CoupledResourceLookUp", self.saxon_proc.make_string_value("disabled") ) + @property + @override + def output_schema(self): + return "http://www.isotc211.org/2005/gmd" + @override def as_dcat(self, tree: PyXdmNode) -> PyXdmNode: return self.xslt_exec.transform_to_value(xdm_node=tree).head From 3633a266df9eac7d1a1cae013169739da6421bda Mon Sep 17 00:00:00 2001 From: streino Date: Wed, 24 Dec 2025 14:43:44 +0100 Subject: [PATCH 03/11] refactor --- udata/harvest/backends/dcat.py | 84 +++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py index a8c4feeb68..20296d564c 100644 --- a/udata/harvest/backends/dcat.py +++ b/udata/harvest/backends/dcat.py @@ -1,4 +1,5 @@ import logging +from abc import abstractmethod from datetime import date from typing import ClassVar, Generator @@ -252,22 +253,14 @@ def get_node_from_item(self, graph, item): raise ValueError(f"Unable to find dataset with DCT.identifier:{item.remote_id}") -class CswDcatBackend(DcatBackend): - """ - CSW harvester fetching records as DCAT. - The parsing of items is then the same as for the DcatBackend. +class BaseCswDcatBackend(DcatBackend): """ + Abstract base CSW to DCAT harvester. - name = "csw-dcat" - display_name = "CSW-DCAT" + Once items are retrieved from CSW, the parsing of these items is the same as DcatBackend. + """ extra_configs = ( - HarvestExtraConfig( - _("GeoDCAT-AP"), - "enable_geodcat", - str, - _("Request GeoDCAT-AP to the CSW server (must be supported by the server)."), - ), HarvestExtraConfig( _("Remote URL prefix"), "remote_url_prefix", @@ -359,15 +352,24 @@ def __init__(self, *args, **kwargs): self.xpath_proc.declare_namespace("csw", CSW_NAMESPACE) @property + @abstractmethod def output_schema(self): - if self.get_extra_config_value("enable_geodcat"): - return str(GEODCAT) - else: - return str(DCAT) + """ + Return the CSW `outputSchema` property. + """ + pass + + @abstractmethod + def as_dcat(self, tree: PyXdmNode) -> PyXdmNode: + """ + Return the input tree as a DCAT tree. + """ + pass + @override def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]: """ - Yield all RDF pages as `Graph` from the source + Yield all RDF pages as `Graph` from the source. """ output_schema = self.output_schema page_number = 0 @@ -408,19 +410,11 @@ def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, N return page_number += 1 - start = self.next_position(start, search_results) + start = self._next_position(start, search_results) if not start: return - def as_dcat(self, tree: PyXdmNode) -> PyXdmNode: - """ - Return the input tree as a DCAT tree. - For CswDcatBackend, this method return the incoming tree as-is, since it's already DCAT. - For subclasses of CswDcatBackend, this method should convert the incoming tree to DCAT. - """ - return tree - - def next_position(self, start: int, search_results: PyXdmNode) -> int | None: + def _next_position(self, start: int, search_results: PyXdmNode) -> int | None: next_record = int(search_results.get_attribute_value("nextRecord")) matched_count = int(search_results.get_attribute_value("numberOfRecordsMatched")) returned_count = int(search_results.get_attribute_value("numberOfRecordsReturned")) @@ -442,17 +436,45 @@ def next_position(self, start: int, search_results: PyXdmNode) -> int | None: return None if should_break else next_record -class CswIso19139DcatBackend(CswDcatBackend): +class CswDcatBackend(BaseCswDcatBackend): + """ + CSW harvester fetching records as DCAT. + """ + + name = "csw-dcat" + display_name = "CSW-DCAT" + + extra_configs = ( + *BaseCswDcatBackend.extra_configs, + HarvestExtraConfig( + _("GeoDCAT-AP"), + "enable_geodcat", + str, + _("Request GeoDCAT-AP to the CSW server (must be supported by the server)."), + ), + ) + + @property + @override + def output_schema(self): + if self.get_extra_config_value("enable_geodcat"): + return str(GEODCAT) + else: + return str(DCAT) + + @override + def as_dcat(self, tree: PyXdmNode) -> PyXdmNode: + return tree + + +class CswIso19139DcatBackend(BaseCswDcatBackend): """ CSW harvester fetching records as ISO-19139 and using XSLT to convert them to DCAT. - The parsing of items is then the same as for the DcatBackend. """ name = "csw-iso-19139" display_name = "CSW-ISO-19139" - extra_configs = [c for c in CswDcatBackend.extra_configs if c.key != "enable_geodcat"] - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) xslt_url = current_app.config["HARVEST_ISO19139_XSLT_URL"] From 34ca67ade01f142dfa559e9e6e772186b482a6f0 Mon Sep 17 00:00:00 2001 From: streino Date: Wed, 24 Dec 2025 15:15:49 +0100 Subject: [PATCH 04/11] to_bool --- udata/harvest/backends/dcat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py index 20296d564c..250aa60fdc 100644 --- a/udata/harvest/backends/dcat.py +++ b/udata/harvest/backends/dcat.py @@ -25,6 +25,7 @@ url_from_rdf, ) from udata.storage.s3 import store_as_json +from udata.utils import to_bool from .base import BaseBackend, HarvestExtraConfig @@ -457,7 +458,7 @@ class CswDcatBackend(BaseCswDcatBackend): @property @override def output_schema(self): - if self.get_extra_config_value("enable_geodcat"): + if to_bool(self.get_extra_config_value("enable_geodcat")): return str(GEODCAT) else: return str(DCAT) From e917dd01a422dd6e2886369a19fed191fed102e8 Mon Sep 17 00:00:00 2001 From: streino Date: Wed, 24 Dec 2025 15:15:31 +0100 Subject: [PATCH 05/11] fixtures cleanup --- ...xml => geoide-iso19139-single-dataset.xml} | 0 ...-page-1.xml => geonetwork-dcat-page-1.xml} | 0 ...-page-3.xml => geonetwork-dcat-page-3.xml} | 0 ...-page-5.xml => geonetwork-dcat-page-5.xml} | 0 ...e-1.xml => geonetwork-iso19139-page-1.xml} | 0 ...e-3.xml => geonetwork-iso19139-page-3.xml} | 0 ...e-5.xml => geonetwork-iso19139-page-5.xml} | 0 udata/harvest/tests/test_dcat_backend.py | 32 +++++++------------ 8 files changed, 11 insertions(+), 21 deletions(-) rename udata/harvest/tests/csw_dcat/{geo-ide_single-dataset.xml => geoide-iso19139-single-dataset.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetworkv4-page-1.xml => geonetwork-dcat-page-1.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetworkv4-page-3.xml => geonetwork-dcat-page-3.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetworkv4-page-5.xml => geonetwork-dcat-page-5.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetwork-iso-page-1.xml => geonetwork-iso19139-page-1.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetwork-iso-page-3.xml => geonetwork-iso19139-page-3.xml} (100%) rename udata/harvest/tests/csw_dcat/{geonetwork-iso-page-5.xml => geonetwork-iso19139-page-5.xml} (100%) diff --git a/udata/harvest/tests/csw_dcat/geo-ide_single-dataset.xml b/udata/harvest/tests/csw_dcat/geoide-iso19139-single-dataset.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geo-ide_single-dataset.xml rename to udata/harvest/tests/csw_dcat/geoide-iso19139-single-dataset.xml diff --git a/udata/harvest/tests/csw_dcat/geonetworkv4-page-1.xml b/udata/harvest/tests/csw_dcat/geonetwork-dcat-page-1.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetworkv4-page-1.xml rename to udata/harvest/tests/csw_dcat/geonetwork-dcat-page-1.xml diff --git a/udata/harvest/tests/csw_dcat/geonetworkv4-page-3.xml b/udata/harvest/tests/csw_dcat/geonetwork-dcat-page-3.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetworkv4-page-3.xml rename to udata/harvest/tests/csw_dcat/geonetwork-dcat-page-3.xml diff --git a/udata/harvest/tests/csw_dcat/geonetworkv4-page-5.xml b/udata/harvest/tests/csw_dcat/geonetwork-dcat-page-5.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetworkv4-page-5.xml rename to udata/harvest/tests/csw_dcat/geonetwork-dcat-page-5.xml diff --git a/udata/harvest/tests/csw_dcat/geonetwork-iso-page-1.xml b/udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-1.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetwork-iso-page-1.xml rename to udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-1.xml diff --git a/udata/harvest/tests/csw_dcat/geonetwork-iso-page-3.xml b/udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-3.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetwork-iso-page-3.xml rename to udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-3.xml diff --git a/udata/harvest/tests/csw_dcat/geonetwork-iso-page-5.xml b/udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-5.xml similarity index 100% rename from udata/harvest/tests/csw_dcat/geonetwork-iso-page-5.xml rename to udata/harvest/tests/csw_dcat/geonetwork-iso19139-page-5.xml diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index e63eedb353..f91ecc5a70 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -966,8 +966,8 @@ def test_connection_errors_are_handled_without_sentry(self, rmock, mocker, excep @pytest.mark.options(HARVESTER_BACKENDS=["csw*"]) class CswDcatBackendTest(PytestOnlyDBTestCase): - def test_geonetworkv4(self, rmock): - url = mock_csw_pagination(rmock, "geonetwork/srv/eng/csw.rdf", "geonetworkv4-page-{}.xml") + def test_geonetwork_dcat(self, rmock): + url = mock_csw_pagination(rmock, "geonetwork/srv/fre/csw", "geonetwork-dcat-page-{}.xml") org = OrganizationFactory() source = HarvestSourceFactory(backend="csw-dcat", url=url, organization=org) @@ -1014,7 +1014,7 @@ def test_geonetworkv4(self, rmock): assert resource.type == "main" def test_user_agent_post(self, rmock): - url = mock_csw_pagination(rmock, "geonetwork/srv/eng/csw.rdf", "geonetworkv4-page-{}.xml") + url = mock_csw_pagination(rmock, "geonetwork/srv/fre/csw", "geonetwork-dcat-page-{}.xml") get_mock = rmock.post(url) org = OrganizationFactory() source = HarvestSourceFactory(backend="csw-dcat", url=url, organization=org) @@ -1151,14 +1151,7 @@ def test_url_prefix(self, rmock, remote_url_prefix: str): source = HarvestSourceFactory( backend="csw-dcat", - config={ - "extra_configs": [ - { - "key": "remote_url_prefix", - "value": remote_url_prefix, - } - ] - }, + config={"extra_configs": [{"key": "remote_url_prefix", "value": remote_url_prefix}]}, ) actions.run(source) @@ -1191,21 +1184,16 @@ class CswIso19139DcatBackendTest(PytestOnlyDBTestCase): def test_geo2france(self, rmock, remote_url_prefix: str): with open(os.path.join(CSW_DCAT_FILES_DIR, "XSLT.xml"), "r") as f: xslt = f.read() - url = mock_csw_pagination(rmock, "geonetwork/srv/eng/csw.rdf", "geonetwork-iso-page-{}.xml") + url = mock_csw_pagination( + rmock, "geonetwork/srv/fre/csw", "geonetwork-iso19139-page-{}.xml" + ) rmock.get(current_app.config.get("HARVEST_ISO19139_XSLT_URL"), text=xslt) org = OrganizationFactory() source = HarvestSourceFactory( backend="csw-iso-19139", url=url, organization=org, - config={ - "extra_configs": [ - { - "key": "remote_url_prefix", - "value": remote_url_prefix, - } - ] - }, + config={"extra_configs": [{"key": "remote_url_prefix", "value": remote_url_prefix}]}, ) actions.run(source) @@ -1313,7 +1301,9 @@ def test_geo_ide(self): with open(os.path.join(CSW_DCAT_FILES_DIR, "XSLT.xml"), "rb") as f: xslt = f.read() - with open(os.path.join(CSW_DCAT_FILES_DIR, "geo-ide_single-dataset.xml"), "rb") as f: + with open( + os.path.join(CSW_DCAT_FILES_DIR, "geoide-iso19139-single-dataset.xml"), "rb" + ) as f: csw = f.read() # apply xslt transformation manually instead of using the harvest backend since we're only processing one dataset From ecaa0fe3783d026b1eb886b4c7a6745ea24c8049 Mon Sep 17 00:00:00 2001 From: streino Date: Wed, 24 Dec 2025 15:42:41 +0100 Subject: [PATCH 06/11] get_format --- udata/harvest/backends/dcat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py index 250aa60fdc..555192e7d3 100644 --- a/udata/harvest/backends/dcat.py +++ b/udata/harvest/backends/dcat.py @@ -128,7 +128,7 @@ def inner_harvest(self): else: self.job.data["graphs"] = serialized_graphs - def get_format(self): + def get_format(self) -> str: fmt = guess_format(self.source.url) # if format can't be guessed from the url # we fallback on the declared Content-Type @@ -367,6 +367,10 @@ def as_dcat(self, tree: PyXdmNode) -> PyXdmNode: """ pass + @override + def get_format(self) -> str: + return "xml" + @override def walk_graph(self, url: str, fmt: str) -> Generator[tuple[int, Graph], None, None]: """ From d861ddd2ba578caf41c2efdaea8487d7f593e9e6 Mon Sep 17 00:00:00 2001 From: streino Date: Wed, 24 Dec 2025 15:48:12 +0100 Subject: [PATCH 07/11] wip test geodcat option --- .../csw_dcat/geonetwork-geodcatap-page-1.xml | 314 ++++++++++++++++ .../csw_dcat/geonetwork-geodcatap-page-3.xml | 336 ++++++++++++++++++ .../csw_dcat/geonetwork-geodcatap-page-5.xml | 330 +++++++++++++++++ udata/harvest/tests/test_dcat_backend.py | 20 ++ 4 files changed, 1000 insertions(+) create mode 100644 udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml create mode 100644 udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml create mode 100644 udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml new file mode 100644 index 0000000000..00e6ed182a --- /dev/null +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml @@ -0,0 +1,314 @@ + + + + + + + + + + Dataset + + + + + + + 04bcec79-5b25-4b16-b635-73115f7456e4 + 2021-01-25T14:54:30 + INSEE - Part des ménages présents depuis 5 ans ou plus dans leur logement actuel (2010) + Part des ménages présents depuis 5 ans ou plus dans leur logement actuel, par "rectangle INSEE" en Région Hauts-de-France. La taille des cercles proportionnels est relative au nombre de ménages. Le nombre de ménages présents depuis 5 ans ou plus dans leur logement actuel est indiqué dans chaque cercle. Le "rectangle" est l'objet élémentaire des données carroyées à 200 mètres de l'INSEE relatives aux ménages (Millésime 2010). + + + + + + + Géo2France + + + Géo2France + + + + + + + + + + + + + + + INSEE - Part des ménages présents depuis 5 ans ou plus dans leur logement actuel (2010) + 2020-09-22 + https://www.geo2france.fr/insee/partmenage5ans + Part des ménages présents depuis 5 ans ou plus dans leur logement actuel, par "rectangle INSEE" en Région Hauts-de-France. La taille des cercles proportionnels est relative au nombre de ménages. Le nombre de ménages présents depuis 5 ans ou plus dans leur logement actuel est indiqué dans chaque cercle. Le "rectangle" est l'objet élémentaire des données carroyées à 200 mètres de l'INSEE relatives aux ménages (Millésime 2010). + + + + Géo2France + + + Géo2France + + + + + + LOGEMENT + INSTITUT NATIONAL DE LA STATISTIQUE ET DES ETUDES ECONOMIQUES + MENAGE + POPULATION + INSEE + DONNEE OUVERTE + HAUTS-DE-FRANCE + + + + + + Source : données carroyées à 200 mètres de l'INSEE. Mise en ligne et représentation : Géo2France. + + + + + Population et société + + + + + + + + {"type":"Polygon","coordinates":[[[1.32898744824151,51.0829997967797],[4.30533171628197,51.0829997967797],[4.30533171628197,48.79422289313],[1.32898744824151,48.79422289313],[1.32898744824151,51.0829997967797]]]} + + + + + insee + + + + + Métadonnées INSEE - data.gouv.fr + + + + + insee.fr - données carroyées à 200 mètres + + + + + Documentation générale - données carroyées + Documentation générale - données carroyées + + + + + Documentation complète - données carroyées à 200 mètres + + + + + rectangles_200m_menage_erbm + INSEE - Part des ménages présents depuis plus de 5 ans + + + + + rectangles_200m_menage_erbm + + + + + + + + + + + insee:rectangles_200m_menage_erbm + INSEE - Part des ménages présents depuis plus de 5 ans + + + + + insee:rectangles_200m_menage_erbm + + + + + + + + + + + + + + + + Dataset + + + + + + + 06367a09-096c-4db6-a122-52469176d619 + 2024-02-07T17:22:30.529297Z + Zonage du bassin urbain à dynamiser (BUD) de la Région Hauts-de-France - (2018-2020) + Zonage défini par un arrêté de 2018. Les entreprises qui se créent sur les communes du bassin urbain à dynamiser (entre le 1/1/2018 et le 31/12/2020) bénéficient pendant 3 ans d'un régime spécifique d'exonération fiscale. Dans les Hauts-de-France 159 communes du bassin minier sont concernées par ce dispositif. +Arrêté officiel de classement : https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000038436169&categorieLien=id + + + + + + + + Géo2France + + + Géo2France + + + https://www.geo2france.fr + + + + + + + + + + + + Zonage du bassin urbain à dynamiser (BUD) de la Région Hauts-de-France - (2018-2020) + 2020-05-28 + fr-200053742/2019/limite/bud + Zonage défini par un arrêté de 2018. Les entreprises qui se créent sur les communes du bassin urbain à dynamiser (entre le 1/1/2018 et le 31/12/2020) bénéficient pendant 3 ans d'un régime spécifique d'exonération fiscale. Dans les Hauts-de-France 159 communes du bassin minier sont concernées par ce dispositif. +Arrêté officiel de classement : https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000038436169&categorieLien=id + + + + Géo2France + + + Géo2France + + + + + + COMMUNE + BUD + BASSIN URBAIN A DYNAMISER + ECONOMIE + ENTREPRISE + FISCALITE + EMPLOI + AIDE ECONOMIQUE + BASSIN MINIER + DONNEE OUVERTE + ECONOMIE + Unités administratives + HAUTS-DE-FRANCE + PAS-DE-CALAIS + NORD + + + Utilisation libre sous réserve de mentionner la source (a minima le nom du producteur) et la date de sa dernière mise à jour + + + + + Données ouvertes + + + + + + + + Source CGET +Communes en bassin urbain à dynamiser (159 communes concernées) avec contours basés sur la BD Topo de l’IGN. (05-2020) + + + + + Économie et finances + + + + + + + {"type":"Polygon","coordinates":[[[1.365,51.09],[4.086,51.09],[4.086,48.827],[1.365,48.827],[1.365,51.09]]]} + + + + + + + + bud + + + + + bassins_urbains_a_dynamiser + Bassins urbains à dynamiser (BUD) + + + + + bassins_urbains_a_dynamiser + + + + + + + + + + + + + + + + anct:bassins_urbains_a_dynamiser + Bassins urbains à dynamiser (BUD) + + + + + anct:bassins_urbains_a_dynamiser + + + + + + + + + + + + + + + + + diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml new file mode 100644 index 0000000000..ff267c9ee5 --- /dev/null +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml @@ -0,0 +1,336 @@ + + + + + + + + + + Dataset + + + + + + + 093789e0-af10-4861-b661-febdaf543c5c + 2024-08-02T08:54:23.954134Z + Communes en état de catastrophe naturelle au 30 novembre 2023 + Communes en état de catastrophe naturelle au 30 novembre 2023, suite aux inondations dans le Pas de Calais et le Nord. + + + + + + + + + Géo2France + + + + + + + + + + + + + + + Communes en état de catastrophe naturelle au 30 novembre 2023 + 2023-11-14 + https://www.geo2france.fr/risque/catnat2023 + Communes en état de catastrophe naturelle au 30 novembre 2023, suite aux inondations dans le Pas de Calais et le Nord. + + + Finalisé + + + + + + + + Géo2France + + + + + + + + catastrophe naturelle + + + + + inondation + + + + + Commune française + + + catnat + inondation + + + + + + Licence Ouverte version 2.0 https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf + + + + + + + + Limites de communes BD Topo 2023. + + + 5 + + + + {"type":"Polygon","coordinates":[[[1.26761534,51.26912631],[3.25154517,51.26912631],[3.25154517,50.25455878],[1.26761534,50.25455878],[1.26761534,51.26912631]]]} + + + + + + + + com_catnat_12_2023 + Communes en état de catastrophe naturelle en novembre 2023 + + + + + com_catnat_12_2023 + + + + + + + + + + + + + + + + geo2france:com_catnat_12_2023 + Communes en état de catastrophe naturelle en novembre 2023 + + + + + geo2france:com_catnat_12_2023 + + + + + + + + + + + + + + + + Arrêté du ministère de l'intérieur + Liste des 205 communes + + + + + + + + + + Dataset + + + + + + + 0ac1d658-b8ce-4815-975b-1dd401ce01ed + 2024-02-12T15:27:29.272037Z + Etablissements d'enseignement + Liste géolocalisée des établissements d'enseignement des premier et second degrés (tous ministères de tutelle, secteurs public et privé) situés en France. Les établissements concernés sont déterminés selon leur code nature dans la nomenclature de l'éducation nationale. Les codes entre 100 et 199 (premier degré) et entre 300 et 399 (second degré) sont retenus. + +Donnée téléchargée quotidiennement depuis data.gouv.fr, découpée sur le territoire de la région Hauts-de-France, et publié par Géo2France. + + + + + + + + + Géo2France + + + + + + + + + + + + + + + Etablissements d'enseignement + 2024-02-12 + 0ac1d658-b8ce-4815-975b-1dd401ce01ed + http://localhost:8080/demodem/editioin + Liste géolocalisée des établissements d'enseignement des premier et second degrés (tous ministères de tutelle, secteurs public et privé) situés en France. Les établissements concernés sont déterminés selon leur code nature dans la nomenclature de l'éducation nationale. Les codes entre 100 et 199 (premier degré) et entre 300 et 399 (second degré) sont retenus. + +Donnée téléchargée quotidiennement depuis data.gouv.fr, découpée sur le territoire de la région Hauts-de-France, et publié par Géo2France. + + + Mis à jour continue + + + + + + + + Géo2France + + + + + + donnée ouverte + Services d'utilité publique et services publics + + + éducation + + + + + enseignement scolaire + + + + + Point d'intérêt + + + + + + + + Source : RAMSESE. Donnée d'origine publiée sur data.gouv.fr + + + + + Agriculture, pêche, sylviculture et alimentation + + + + + Environnement + + + + + Régions et villes + + + + + + + + + + + {"type":"Polygon","coordinates":[[[1.3802175217118144,51.088989439843324],[4.25583684779586,51.088989439843324],[4.25583684779586,48.83720744013672],[1.3802175217118144,48.83720744013672],[1.3802175217118144,51.088989439843324]]]} + + + + + + + + + + + etablissement_enseignement + Etablissements d'enseignement du premier et second degrés + + + + + etablissement_enseignement + + + + + + + + + + + + + + + + Lien vers la donnée originale sur data.gouv + jeu de donnée complet + + + + + vrt-bot:etablissement_enseignement + Etablissements d'enseignement du premier et second degrés + + + + + vrt-bot:etablissement_enseignement + + + + + + + + + + + + + + + + + diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml new file mode 100644 index 0000000000..828f8411bc --- /dev/null +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml @@ -0,0 +1,330 @@ + + + + + + + + + + Dataset + + + + + + + 0ae299e7-10d6-4290-944e-c6c62e2aeabf + 2025-08-21T14:44:26.275862Z + Territoires d'exercice de la compétence "Collecte des déchets ménagers" + L'article L2224-13 du Code général des collectivités territoriales autorise un transfère partiel de la compétence de collecte et de traitement des déchets des ménages. Il est ainsi possible pour les EPCI à fiscalité propre, de ne transférer que la partie de la compétence comprenant le traitement, ou seulement celle comportant la collecte. La gestion des déchetteries étant à l'interface entre la collecte et le traitement, elle peut être rattachée à l'une ou l'autre de ces missions. +Ainsi, on distingue 3 types de territoires différents : les territoires de collecte, les territoires de traitement, et les territoires de gestion des déchetteries. +Ces contours ne correspondent pas nécessairement aux contours des EPCI ou syndicats mixtes. + + + + + + + + + Géo2France + + + + + + + + + + + + + + + Territoires d'exercice de la compétence "Collecte des déchets ménagers" + 2023 + 0ae299e7-10d6-4290-944e-c6c62e2aeabf + L'article L2224-13 du Code général des collectivités territoriales autorise un transfère partiel de la compétence de collecte et de traitement des déchets des ménages. Il est ainsi possible pour les EPCI à fiscalité propre, de ne transférer que la partie de la compétence comprenant le traitement, ou seulement celle comportant la collecte. La gestion des déchetteries étant à l'interface entre la collecte et le traitement, elle peut être rattachée à l'une ou l'autre de ces missions. +Ainsi, on distingue 3 types de territoires différents : les territoires de collecte, les territoires de traitement, et les territoires de gestion des déchetteries. +Ces contours ne correspondent pas nécessairement aux contours des EPCI ou syndicats mixtes. + + + Mis à jour continue + + + + + + + + Odema + + + + + + + + + + + Géo2France + + + + + + déchets + déchets assimilés aux ordures ménagères + collecte des déchets + + + Hauts-de-France (Région) + + + odema + + + + + + Licence Ouverte version 2.0 https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf + + + + + + + + Consolidation des informations Sinoe, avec veille locale de l'Observatoire déchets-matières des Hauts-de-France (Odema). + + + + + + {"type":"Polygon","coordinates":[[[1.3802175217118144,51.088989439843324],[4.25583684779586,51.088989439843324],[4.25583684779586,48.83720744013672],[1.3802175217118144,48.83720744013672],[1.3802175217118144,51.088989439843324]]]} + + + + + + + + + + + territoires_collecte + Territoires de collecte + + + + + territoires_collecte + + + + + + + + + + + odema:territoires_collecte + + + + + odema:territoires_collecte + + + + + + + + + + + Visionneuse cartographique Odema + + + + + + + + + + Dataset + + + + + + + 0c232513-cb8c-473a-bbdd-8435eb3987cc + 2023-04-26T07:40:35 + Plages sans plastique en Hauts-de-France en 2022 + Localisation des plages en région Hauts-de-France signataires de la charte d'engagement nationale "plages sans déchets plastique" +Dernière données disponibles au 01/07/2022 +Plus de renseignements : https://www.ecologie.gouv.fr/plages-sans-dechet-plastique-charte-communes-eco-exemplaires + + + + + + + + + Région Hauts-de-France + + + + + + + + + + + + + + + Plages sans plastique en Hauts-de-France en 2022 + 2022-10-17 + 2023-02-15 + fr-200053742/2022/societe/plage_sans_plastique + Localisation des plages en région Hauts-de-France signataires de la charte d'engagement nationale "plages sans déchets plastique" +Dernière données disponibles au 01/07/2022 +Plus de renseignements : https://www.ecologie.gouv.fr/plages-sans-dechet-plastique-charte-communes-eco-exemplaires + + + Finalisé + + + + + + + + Géo2France + + + + + + SOCIETE + DECHET + PLAGE + PARLEMENT DE LA MER + ENVIRONNEMENT + PLASTIQUE + ECOLOGIE + LABEL ACCUEIL QUALITE BIEN-ETRE + DONNEE OUVERTE + Services d'utilité publique et services publics + HAUTS-DE-FRANCE + PAS-DE-CALAIS + + + Utilisation libre sous réserve de mentionner la source (a minima le nom du producteur) et la date de sa dernière mise à jour + + + + + Données ouvertes + + + + + + + + Données Ministère de la transition écologique et solidaire +BD TOPO. 2020-11 + + + + + Population et société + + + + + + + {"type":"Polygon","coordinates":[[[1.36,51.0911],[4.086,51.0911],[4.086,48.827],[1.36,48.827],[1.36,51.0911]]]} + + + + + + + + + + + Tableau de données (csv) + Données brutes au format Csv (Plage sans plastique) et encodage UTF-8 + + + + + plage_sans_plastique + plage sans plastique + + + + + plage_sans_plastique + + + + + + + + + + + plage_sans_plastique + plage sans plastique + + + + + plage_sans_plastique + + + + + + + + + + + geojson + Données format Geojson (plage sans plastique) + + + + + Label Plage sans plastique (shapefile) + Données format Shapefile Plage sans plastique + + + + + Visualisation de la donnée + Visionneuse cartographique Plages sans plastique + + + + + + diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index f91ecc5a70..0ee016305c 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -1013,6 +1013,26 @@ def test_geonetwork_dcat(self, rmock): assert resource.format == "ogc:wms" assert resource.type == "main" + def test_geonetwork_geodcatap(self, rmock): + url = mock_csw_pagination( + rmock, "geonetwork/srv/fre/csw", "geonetwork-geodcatap-page-{}.xml" + ) + source = HarvestSourceFactory( + backend="csw-dcat", + url=url, + config={"extra_configs": [{"key": "enable_geodcat", "value": "true"}]}, + ) + + actions.run(source) + source.reload() + + job = source.get_last_job() + assert len(job.items) == 6 + + # TODO + # datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} + # assert len(datasets) == 6 + def test_user_agent_post(self, rmock): url = mock_csw_pagination(rmock, "geonetwork/srv/fre/csw", "geonetwork-dcat-page-{}.xml") get_mock = rmock.post(url) From b5f7a82ae7a05d7594c926fdb582712fce8b1503 Mon Sep 17 00:00:00 2001 From: streino Date: Fri, 26 Dec 2025 12:17:46 +0100 Subject: [PATCH 08/11] check output_schema --- udata/harvest/tests/test_dcat_backend.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index 0ee016305c..116ddb400e 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -15,6 +15,8 @@ from udata.core.dataset.factories import DatasetFactory, LicenseFactory, ResourceSchemaMockData from udata.core.dataset.rdf import dataset_from_rdf from udata.core.organization.factories import OrganizationFactory +from udata.harvest.backends import get_backend +from udata.harvest.backends.dcat import CswDcatBackend from udata.harvest.models import HarvestJob from udata.models import Dataset from udata.rdf import DCAT, RDF, namespace_manager @@ -971,8 +973,11 @@ def test_geonetwork_dcat(self, rmock): org = OrganizationFactory() source = HarvestSourceFactory(backend="csw-dcat", url=url, organization=org) - actions.run(source) + backend = get_backend(source.backend)(source) + assert isinstance(backend, CswDcatBackend) + assert backend.output_schema == "http://www.w3.org/ns/dcat#" + actions.run(source) source.reload() job = source.get_last_job() @@ -1023,6 +1028,10 @@ def test_geonetwork_geodcatap(self, rmock): config={"extra_configs": [{"key": "enable_geodcat", "value": "true"}]}, ) + backend = get_backend(source.backend)(source) + assert isinstance(backend, CswDcatBackend) + assert backend.output_schema == "http://data.europa.eu/930/" + actions.run(source) source.reload() From 79a69a88fbdf714a76edf9eb97921da01c58ac10 Mon Sep 17 00:00:00 2001 From: streino Date: Fri, 26 Dec 2025 14:33:52 +0100 Subject: [PATCH 09/11] complete test --- .../csw_dcat/geonetwork-geodcatap-page-1.xml | 4 +-- .../csw_dcat/geonetwork-geodcatap-page-3.xml | 4 +-- .../csw_dcat/geonetwork-geodcatap-page-5.xml | 4 +-- udata/harvest/tests/test_dcat_backend.py | 35 +++++++++++++++++-- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml index 00e6ed182a..3ab9370cb0 100644 --- a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml @@ -1,7 +1,7 @@ - - + + diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml index ff267c9ee5..e8e253271c 100644 --- a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-3.xml @@ -1,7 +1,7 @@ - - + + diff --git a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml index 828f8411bc..3a22ab95d4 100644 --- a/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml +++ b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-5.xml @@ -1,7 +1,7 @@ - - + + diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index 116ddb400e..5128743324 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -1038,9 +1038,38 @@ def test_geonetwork_geodcatap(self, rmock): job = source.get_last_job() assert len(job.items) == 6 - # TODO - # datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - # assert len(datasets) == 6 + datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} + assert len(datasets) == 6 + + # First dataset + dataset = datasets["https://www.geo2france.fr/insee/partmenage5ans"] + assert ( + dataset.title + == "INSEE - Part des ménages présents depuis 5 ans ou plus dans leur logement actuel (2010)" + ) + assert dataset.description.startswith( + "Part des ménages présents depuis 5 ans ou plus dans leur logement actuel" + ) + assert set(dataset.tags) == set( + [ + "logement", + "institut-national-de-la-statistique-et-des-etudes-economiques", + "menage", + "population", + "insee", + "donnee-ouverte", + "hauts-de-france", + "population-et-societe", + ] + ) + assert dataset.harvest.issued_at.date() == date(2020, 9, 22) + assert dataset.harvest.created_at is None + # FIXME: len(resources) should be 2 but they have the same url => last wins + assert len(dataset.resources) == 1 + resource = dataset.resources[0] + assert resource.title == "insee:rectangles_200m_menage_erbm" + assert resource.url == "https://www.geo2france.fr/geoserver/insee/ows" + assert resource.type == "api" def test_user_agent_post(self, rmock): url = mock_csw_pagination(rmock, "geonetwork/srv/fre/csw", "geonetwork-dcat-page-{}.xml") From 927ec543579bd2d1df6e381150315e4b3b2a9406 Mon Sep 17 00:00:00 2001 From: streino Date: Fri, 26 Dec 2025 14:50:06 +0100 Subject: [PATCH 10/11] chore: set([]) to {} --- udata/harvest/tests/test_dcat_backend.py | 83 +++++++++++------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index 5128743324..78f2476ef3 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -452,8 +452,8 @@ def test_harvest_inspire_themese(self, rmock): datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert set(datasets["1"].tags).issuperset(set(["repartition-des-especes", "inspire"])) - assert set(datasets["2"].tags).issuperset(set(["hydrographie", "inspire"])) + assert set(datasets["1"].tags).issuperset({"repartition-des-especes", "inspire"}) + assert set(datasets["2"].tags).issuperset({"hydrographie", "inspire"}) assert "inspire" not in datasets["3"].tags def test_simple_nested_attributes(self, rmock): @@ -683,9 +683,10 @@ def test_geonetwork_xml_catalog(self, rmock): assert dataset.temporal_coverage is not None assert dataset.temporal_coverage.start == date(2004, 11, 3) assert dataset.temporal_coverage.end == date(2005, 3, 30) - assert set(dataset.tags) == set( - ["inspire", "biodiversity-dynamics"] - ) # The DCAT.theme with rdf:resource don't have labels properly defined + assert set(dataset.tags) == { + "inspire", + "biodiversity-dynamics", + } # The DCAT.theme with rdf:resource don't have labels properly defined def test_sigoreme_xml_catalog(self, rmock): LicenseFactory(id="fr-lo", title="Licence ouverte / Open Licence") @@ -993,22 +994,20 @@ def test_geonetwork_dcat(self, rmock): assert ( dataset.description == "Accidents corporels de la circulation en Hauts de France (2017)" ) - assert set(dataset.tags) == set( - [ - "donnee-ouverte", - "accidentologie", - "accident", - "reseaux-de-transport", - "accident-de-la-route", - "hauts-de-france", - "nord", - "pas-de-calais", - "oise", - "somme", - "aisne", - # "inspire", TODO: the geonetwork v4 examples use broken URI as theme resources, check if this is still a problem or not - ] - ) + assert set(dataset.tags) == { + "donnee-ouverte", + "accidentologie", + "accident", + "reseaux-de-transport", + "accident-de-la-route", + "hauts-de-france", + "nord", + "pas-de-calais", + "oise", + "somme", + "aisne", + # "inspire", TODO: the geonetwork v4 examples use broken URI as theme resources, check if this is still a problem or not + } assert dataset.harvest.issued_at.date() == date(2017, 1, 1) assert dataset.harvest.created_at is None assert len(dataset.resources) == 1 @@ -1050,18 +1049,16 @@ def test_geonetwork_geodcatap(self, rmock): assert dataset.description.startswith( "Part des ménages présents depuis 5 ans ou plus dans leur logement actuel" ) - assert set(dataset.tags) == set( - [ - "logement", - "institut-national-de-la-statistique-et-des-etudes-economiques", - "menage", - "population", - "insee", - "donnee-ouverte", - "hauts-de-france", - "population-et-societe", - ] - ) + assert set(dataset.tags) == { + "logement", + "institut-national-de-la-statistique-et-des-etudes-economiques", + "menage", + "population", + "insee", + "donnee-ouverte", + "hauts-de-france", + "population-et-societe", + } assert dataset.harvest.issued_at.date() == date(2020, 9, 22) assert dataset.harvest.created_at is None # FIXME: len(resources) should be 2 but they have the same url => last wins @@ -1275,17 +1272,15 @@ def test_geo2france(self, rmock, remote_url_prefix: str): dataset.description == "Le présent standard de données COVADIS concerne les documents de plans locaux d'urbanisme (PLU) et les plans d'occupation des sols (POS qui valent PLU)." ) - assert set(dataset.tags) == set( - [ - "amenagement-urbanisme-zonages-planification", - "cartigny", - "document-durbanisme", - "donnees-ouvertes", - "plu", - "usage-des-sols", - "inspire", - ] - ) + assert set(dataset.tags) == { + "amenagement-urbanisme-zonages-planification", + "cartigny", + "document-durbanisme", + "donnees-ouvertes", + "plu", + "usage-des-sols", + "inspire", + } assert dataset.harvest.issued_at.date() == date(2017, 10, 7) assert dataset.harvest.created_at.date() == date(2013, 3, 8) assert dataset.spatial.geom == { From 1ef27f848fc6c8e62c6a5839d3ccf160d76ddfb4 Mon Sep 17 00:00:00 2001 From: streino Date: Fri, 26 Dec 2025 14:50:27 +0100 Subject: [PATCH 11/11] chore: somewhat more consistent spacing --- udata/harvest/tests/test_dcat_backend.py | 58 +++++++++--------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/udata/harvest/tests/test_dcat_backend.py b/udata/harvest/tests/test_dcat_backend.py index 78f2476ef3..f52b32c0e7 100644 --- a/udata/harvest/tests/test_dcat_backend.py +++ b/udata/harvest/tests/test_dcat_backend.py @@ -80,14 +80,12 @@ def test_simple_flat(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() assert len(job.items) == 3 datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 3 for i in "1 2 3".split(): @@ -131,8 +129,8 @@ def test_flat_with_blank_nodes(self, rmock): actions.run(source) datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 3 + assert len(datasets["1"].resources) == 2 assert len(datasets["2"].resources) == 2 assert len(datasets["3"].resources) == 1 @@ -162,8 +160,8 @@ def test_flat_with_blank_nodes_xml(self, rmock): actions.run(source) datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 3 + assert len(datasets["3"].resources) == 1 assert len(datasets["1"].resources) == 2 assert len(datasets["2"].resources) == 2 @@ -179,8 +177,8 @@ def test_harvest_dataservices(self, rmock): actions.run(source) dataservices = Dataservice.objects - assert len(dataservices) == 1 + assert dataservices[0].title == "Explore API v2" assert dataservices[0].base_api_url == "https://data.paris2024.org/api/explore/v2.1/" assert ( @@ -223,7 +221,6 @@ def test_harvest_dataservices_keep_attached_associated_datasets(self, rmock): ) actions.run(source) - existing_dataservice.reload() assert len(Dataservice.objects) == 1 @@ -242,7 +239,6 @@ def test_harvest_dataservices_ignore_accessservices(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() @@ -260,6 +256,7 @@ def test_harvest_literal_spatial(self, rmock): datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} assert len(datasets) == 8 + assert ( datasets[ "https://www.arcgis.com/home/item.html?id=f6565516d1354383b25793e630cf3f2b&sublayer=5" @@ -311,7 +308,6 @@ def test_harvest_big_catalog(self, rmock): actions.run(source) datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert datasets["1"].schema is None resources_by_title = {resource["title"]: resource for resource in datasets["1"].resources} @@ -462,7 +458,6 @@ def test_simple_nested_attributes(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=OrganizationFactory()) actions.run(source) - source.reload() job = source.get_last_job() @@ -493,7 +488,6 @@ def test_idempotence(self, rmock): actions.run(source) datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 3 assert len(datasets["1"].resources) == 2 assert len(datasets["2"].resources) == 2 @@ -505,7 +499,6 @@ def test_hydra_partial_collection_view_pagination(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() @@ -517,7 +510,6 @@ def test_hydra_legacy_paged_collection_pagination(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() @@ -530,11 +522,9 @@ def test_failure_on_initialize(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() - assert job.status == "failed" def test_supported_mime_type(self, rmock): @@ -544,11 +534,9 @@ def test_supported_mime_type(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() - assert job.status == "done" assert job.errors == [] assert len(job.items) == 4 @@ -659,7 +647,9 @@ def test_geonetwork_xml_catalog(self, rmock): url = mock_dcat(rmock, "geonetwork.xml", path="catalog.xml") org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) + actions.run(source) + dataset = Dataset.objects.filter(organization=org).first() assert dataset is not None assert dataset.harvest is not None @@ -693,9 +683,10 @@ def test_sigoreme_xml_catalog(self, rmock): url = mock_dcat(rmock, "sig.oreme.rdf") org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) + actions.run(source) - dataset = Dataset.objects.filter(organization=org).first() + dataset = Dataset.objects.filter(organization=org).first() assert dataset is not None assert dataset.frequency == UpdateFrequency.IRREGULAR assert "gravi" in dataset.tags # support dcat:keyword @@ -723,9 +714,10 @@ def test_datara_extended_roles_foaf(self, rmock): url = mock_dcat(rmock, "datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml") org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) + actions.run(source) - dataset = Dataset.objects.filter(organization=org).first() + dataset = Dataset.objects.filter(organization=org).first() assert dataset is not None assert len(dataset.contact_points) == 2 @@ -742,9 +734,10 @@ def test_datara_extended_roles_vcard(self, rmock): url = mock_dcat(rmock, "datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml") org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) + actions.run(source) - dataset = Dataset.objects.filter(organization=org).first() + dataset = Dataset.objects.filter(organization=org).first() assert dataset is not None assert len(dataset.contact_points) == 3 @@ -765,15 +758,16 @@ def test_udata_xml_catalog(self, rmock): url = mock_dcat(rmock, "udata.xml") org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) - actions.run(source) + actions.run(source) source.reload() + job = source.get_last_job() assert len(job.items) == 3 assert Dataset.objects.filter(organization=org).count() == 2 - dataset = Dataset.objects.filter(organization=org, title="Bureaux de vote - Vanves").first() + dataset = Dataset.objects.filter(organization=org, title="Bureaux de vote - Vanves").first() assert dataset is not None assert "bureaux-de-vote" in dataset.tags # support dcat:keyword assert len(dataset.resources) == 4 @@ -833,6 +827,7 @@ def test_user_agent_get(self, rmock): get_mock = rmock.get(url) org = OrganizationFactory() source = HarvestSourceFactory(backend="dcat", url=url, organization=org) + actions.run(source) assert "User-Agent" in get_mock.last_request.headers @@ -845,14 +840,11 @@ def test_unsupported_mime_type(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() - assert job.status == "failed" assert len(job.errors) == 1 - error = job.errors[0] assert error.message == 'Unsupported mime type "text/html"' @@ -863,14 +855,11 @@ def test_unable_to_detect_format(self, rmock): source = HarvestSourceFactory(backend="dcat", url=url, organization=org) actions.run(source) - source.reload() job = source.get_last_job() - assert job.status == "failed" assert len(job.errors) == 1 - error = job.errors[0] expected = "Unable to detect format from extension or mime type" assert error.message == expected @@ -900,8 +889,8 @@ def test_use_replaced_uris(self, rmock, mocker): URIS_TO_REPLACE, {}, # Empty dict to test the mechanism exists ) - actions.run(source) + actions.run(source) source.reload() job = source.get_last_job() @@ -985,7 +974,6 @@ def test_geonetwork_dcat(self, rmock): assert len(job.items) == 6 datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 6 # First dataset @@ -1094,10 +1082,9 @@ def test_csw_error(self, rmock): source = HarvestSourceFactory(backend="csw-dcat") actions.run(source) - source.reload() - job = source.get_last_job() + job = source.get_last_job() assert len(job.errors) == 1 assert "Failed to query CSW" in job.errors[0].message assert job.status == "failed" @@ -1128,10 +1115,9 @@ def test_disallow_external_entities(self, rmock): source = HarvestSourceFactory(backend="csw-dcat") actions.run(source) - source.reload() - job = source.get_last_job() + job = source.get_last_job() assert job.status == "done" assert Dataset.objects.first().title == "test" @@ -1160,10 +1146,9 @@ def test_disallow_external_dtd(self, rmock): source = HarvestSourceFactory(backend="csw-dcat") actions.run(source) - source.reload() - job = source.get_last_job() + job = source.get_last_job() assert not any(h.method == "GET" for h in rmock.request_history) assert job.status == "done" assert len(job.items) == 1 @@ -1211,6 +1196,7 @@ def test_url_prefix(self, rmock, remote_url_prefix: str): actions.run(source) source.reload() + job = source.get_last_job() assert len(job.items) == 1 @@ -1252,14 +1238,12 @@ def test_geo2france(self, rmock, remote_url_prefix: str): ) actions.run(source) - source.reload() job = source.get_last_job() assert len(job.items) == 6 datasets = {d.harvest.dct_identifier: d for d in Dataset.objects} - assert len(datasets) == 6 # First dataset