diff --git a/udata/harvest/backends/dcat.py b/udata/harvest/backends/dcat.py
index 37455e42fe..555192e7d3 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
@@ -15,6 +16,7 @@
from udata.rdf import (
DCAT,
DCT,
+ GEODCAT,
HYDRA,
SPDX,
guess_format,
@@ -23,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
@@ -125,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
@@ -251,14 +254,21 @@ def get_node_from_item(self, graph, item):
raise ValueError(f"Unable to find dataset with DCT.identifier:{item.remote_id}")
-class CswDcatBackend(DcatBackend):
+class BaseCswDcatBackend(DcatBackend):
"""
- CSW harvester fetching records as DCAT.
- The parsing of items is then the same as for the DcatBackend.
+ Abstract base CSW to DCAT harvester.
+
+ Once items are retrieved from CSW, the parsing of these items is the same as DcatBackend.
"""
- name = "csw-dcat"
- 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].
@@ -324,8 +334,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",
@@ -344,15 +352,36 @@ def __init__(self, *args, **kwargs):
self.xpath_proc = self.saxon_proc.new_xpath_processor()
self.xpath_proc.declare_namespace("csw", CSW_NAMESPACE)
+ @property
+ @abstractmethod
+ def output_schema(self):
+ """
+ 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 get_format(self) -> str:
+ return "xml"
+
+ @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
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()
@@ -386,19 +415,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"))
@@ -420,25 +441,44 @@ 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 ISO-19139 and using XSLT to convert them to DCAT.
- The parsing of items is then the same as for the DcatBackend.
+ CSW harvester fetching records as DCAT.
"""
- name = "csw-iso-19139"
- display_name = "CSW-ISO-19139"
+ name = "csw-dcat"
+ display_name = "CSW-DCAT"
extra_configs = (
+ *BaseCswDcatBackend.extra_configs,
HarvestExtraConfig(
- _("Remote URL prefix"),
- "remote_url_prefix",
+ _("GeoDCAT-AP"),
+ "enable_geodcat",
str,
- _("A prefix used to build the remote URL of the harvested items."),
+ _("Request GeoDCAT-AP to the CSW server (must be supported by the server)."),
),
)
- CSW_OUTPUT_SCHEMA = "http://www.isotc211.org/2005/gmd"
+ @property
+ @override
+ def output_schema(self):
+ if to_bool(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.
+ """
+
+ name = "csw-iso-19139"
+ display_name = "CSW-ISO-19139"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -450,6 +490,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
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-geodcatap-page-1.xml b/udata/harvest/tests/csw_dcat/geonetwork-geodcatap-page-1.xml
new file mode 100644
index 0000000000..3ab9370cb0
--- /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..e8e253271c
--- /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..3a22ab95d4
--- /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/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 c5647a76be..f52b32c0e7 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
@@ -78,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():
@@ -129,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
@@ -160,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
@@ -177,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 (
@@ -221,7 +221,6 @@ def test_harvest_dataservices_keep_attached_associated_datasets(self, rmock):
)
actions.run(source)
-
existing_dataservice.reload()
assert len(Dataservice.objects) == 1
@@ -240,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()
@@ -258,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"
@@ -309,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}
@@ -450,8 +448,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):
@@ -460,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()
@@ -491,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
@@ -503,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()
@@ -515,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()
@@ -528,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):
@@ -542,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
@@ -657,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
@@ -681,18 +673,20 @@ 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")
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
@@ -720,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
@@ -739,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
@@ -762,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
@@ -830,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
@@ -842,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"'
@@ -860,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
@@ -897,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()
@@ -966,20 +958,22 @@ 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)
- 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()
assert len(job.items) == 6
datasets = {d.harvest.dct_identifier: d for d in Dataset.objects}
-
assert len(datasets) == 6
# First dataset
@@ -988,22 +982,20 @@ def test_geonetworkv4(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
@@ -1013,8 +1005,59 @@ def test_geonetworkv4(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"}]},
+ )
+
+ backend = get_backend(source.backend)(source)
+ assert isinstance(backend, CswDcatBackend)
+ assert backend.output_schema == "http://data.europa.eu/930/"
+
+ 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
+ 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) == {
+ "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/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)
@@ -1039,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"
@@ -1073,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"
@@ -1105,14 +1146,69 @@ 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
+ @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.options(HARVESTER_BACKENDS=["csw*"])
class CswIso19139DcatBackendTest(PytestOnlyDBTestCase):
@@ -1129,32 +1225,25 @@ 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)
-
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
@@ -1167,17 +1256,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 == {
@@ -1251,7 +1338,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