-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Import du fichier catalogue du MC #456
Comments
@johanricher J'ai préparé l'import en local. Je mets ci-dessous comment j'ai fait et une série de screenshots du résultat. Si ça te semble OK, une fois que DataPass sera branché sur prod (pour qu'on puisse vérifier), je pourrai lancer l'import sur prod. :-) ProcédureJe l'ai testé en local comme suit.
Le script Cliquer pour afficher le scriptimport argparse
import asyncio
import csv
import datetime as dt
import io
from pathlib import Path
from typing import Dict, List, Optional, TextIO
import yaml
from server.application.catalogs.queries import GetCatalogBySiret
from server.application.catalogs.views import ExtraFieldView
from server.config.di import bootstrap, resolve
from server.domain.common.types import id_factory
from server.domain.datasets.entities import DataFormat, UpdateFrequency
from server.domain.organizations.types import Siret
from server.seedwork.application.messages import MessageBus
from tools.initdata import InitData
def _map_geographical_coverage(value: Optional[str]) -> str:
if not value or value == "Information manquante":
return "(Information manquante)" # Field is required
return value
def _map_formats(value: Optional[str], notes_f: TextIO) -> List[str]:
if not value or value == "Information manquante":
notes_f.write("Format : (Information manquante)\n")
return [DataFormat.OTHER.value] # At least one value expected.
if value == "oracle et shp":
return [DataFormat.DATABASE.value, DataFormat.FILE_GIS.value]
if value.startswith("la base est sur Oracle"):
return [DataFormat.DATABASE.value]
unknown_formats = []
def _map_format(value: str) -> DataFormat:
dataformat = {
"ods": DataFormat.FILE_TABULAR,
"xls": DataFormat.FILE_TABULAR,
"xlsx": DataFormat.FILE_TABULAR,
"csv": DataFormat.FILE_TABULAR,
"base de données": DataFormat.DATABASE,
"sql": DataFormat.DATABASE,
"mbd (access)": DataFormat.DATABASE,
"odb": DataFormat.DATABASE,
"dbf": DataFormat.DATABASE,
"shapefile": DataFormat.FILE_GIS,
"shape": DataFormat.FILE_GIS,
"shp": DataFormat.FILE_GIS,
"shp ou map info": DataFormat.FILE_GIS,
"geojson": DataFormat.FILE_GIS,
"kml": DataFormat.FILE_GIS,
"access": DataFormat.DATABASE,
}.get(value.lower().strip(), value)
try:
return DataFormat(dataformat)
except ValueError:
unknown_formats.append(value)
return DataFormat.OTHER
result = [_map_format(val.strip()).value for val in value.split(",")]
if unknown_formats:
notes_f.write(
f"Format (valeurs non-reconnues) : {', '.join(unknown_formats)}\n"
)
return result
def _map_contact_emails(text: Optional[str]) -> List[str]:
if text is None:
return []
return [text.strip().replace(">", "")]
def _map_update_frequency(text: Optional[str], notes_f: TextIO) -> Optional[str]:
if text is None or text in ("NSP", "Information manquante"):
return None
value = {
"aucune": UpdateFrequency.NEVER,
"en continu": UpdateFrequency.REALTIME,
"mensuelle": UpdateFrequency.MONTHLY,
"annuelle": UpdateFrequency.YEARLY,
}.get(text.lower(), text)
try:
update_frequency = UpdateFrequency(value)
except ValueError:
notes_f.write(f"Fréquence de mise à jour (valeur originale) : {value}\n")
return None
return update_frequency.value
def _map_last_updated_at(datestring: str = None) -> Optional[dt.datetime]:
if datestring is None:
return None
return dt.datetime.strptime(datestring, "%Y-%M-%d")
def _map_tag_ids(value: str, tags_by_name: Dict[str, dict]) -> List[str]:
tag_ids = []
# "périmètre délimité des abords (PDA), urbanisme; géolocalisation"
# -> {"périmètre délimité des abords (PDA)", "urbanisme", "géolocalisation"}
cleaned_names = set(name.strip() for name in value.replace(";", ",").split(","))
for name in cleaned_names:
try:
tag = tags_by_name[name]
except KeyError:
tag = {"id": str(id_factory()), "params": {"name": name}}
tags_by_name[name] = tag
tag_ids.append(tag["id"])
return tag_ids
def _map_extra_field_values(
row: dict, extra_fields: List[ExtraFieldView]
) -> List[dict]:
return [
{"extra_field_id": str(extra_field.id), "value": value}
for extra_field in extra_fields
if (value := row[extra_field.name])
]
async def main(csv_path: Path, out_path: Path) -> None:
bus = resolve(MessageBus)
siret = Siret("11004601800013")
catalog = await bus.execute(GetCatalogBySiret(siret=siret))
expected_extra_fields = {field.name for field in catalog.extra_fields}
with csv_path.open(
# CSV file has BOM (Byte Order Mark) that would otherwise be read in
# the first column name, see:
# https://stackoverflow.com/questions/40310042/python-read-csv-bom-embedded-into-the-first-key
# encoding="utf-8-sig",
) as f:
reader = csv.DictReader(f, delimiter=",")
fieldnames = list(reader.fieldnames or [])
rows = list(reader)
common_fields = {
"titre",
"description",
"service",
"couv_geo",
"format",
"si",
"contact_service",
"contact_personne",
"freq_maj",
"date_maj",
"url",
"licence",
"mots_cles",
}
actual_extra_fields = (
set(fieldnames)
- common_fields
- {
"nom_orga",
"siret_orga",
"id_alt_orga",
"date_pub",
"producteur_type", # See: https://github.com/etalab/catalogage-donnees-config/pull/22 # noqa
}
)
assert actual_extra_fields == expected_extra_fields
tags_by_name: dict = {}
datasets = []
for row in rows:
assert row["siret_orga"] == str(siret)
assert row["nom_orga"] == "Ministère de la Culture"
assert not row["id_alt_orga"]
import_notes_f = io.StringIO()
params: dict = {}
params["organization_siret"] = str(siret)
params["title"] = row["titre"]
params["description"] = row["description"]
params["service"] = row["service"] or None
params["geographical_coverage"] = _map_geographical_coverage(
row["couv_geo"] or None
)
params["formats"] = _map_formats(row["format"] or None, import_notes_f)
params["technical_source"] = row["si"] or None
params["producer_email"] = row["contact_service"] or None
params["contact_emails"] = _map_contact_emails(row["contact_personne"] or None)
params["update_frequency"] = _map_update_frequency(
row["freq_maj"] or None, import_notes_f
)
params["last_updated_at"] = _map_last_updated_at(row["date_maj"] or None)
params["url"] = row["url"] or None
params["license"] = row["licence"] or None
params["tag_ids"] = _map_tag_ids(row["mots_cles"], tags_by_name)
params["extra_field_values"] = _map_extra_field_values(
row, catalog.extra_fields
)
if output := import_notes_f.getvalue():
notes = "\n".join(["[[ Notes d'import automatique ]]", "", output])
params["description"] = "\n".join([params["description"], "", notes])
pk = id_factory()
datasets.append({"id": str(pk), "params": params})
spec = InitData(
organizations=[],
catalogs=[],
users=[],
tags=list(tags_by_name.values()),
datasets=datasets,
).dict()
out_path.write_text(yaml.safe_dump(spec))
if __name__ == "__main__":
bootstrap()
parser = argparse.ArgumentParser()
parser.add_argument("csv_path", type=Path)
parser.add_argument("out_path", type=Path)
args = parser.parse_args()
asyncio.run(
main(
csv_path=args.csv_path,
out_path=args.out_path,
)
) Aperçu du résultatAu total j'ai 715 jeux de données et 105 tags. Page d'accueilPage de détailLes champs complémentaires s'affichent correctement. J'ai ajouté des "notes d'import" dans la description pour éviter de perdre de l'information quand l'état actuel de l'outil nous y obligerait (par ex, un "format" qui n'est pas parmi l'enum actuel -- On n'a pas encore de ticket pour le transformer sur le mode de couv_geo / licence ?). FormulaireIdem, les champs complémentaires définis dans le schéma du MC s'affichent bien. (Mon screenshot a un peu buggé.) RechercheLes filtres se comportent bien. Pour le champ couv géo, certains jeux de données n'avaient pas de valeur, or ce champ est requis, j'ai donc mis "(Information manquante)". |
Merci @florimondmanca ces notes seront très utile pour documenter l'import #343
Arf, j'avais oublié ça. 😓 Quelle plaie ces enums. C'est seulement le champ "fréquence de mise à jour" qui est concerné par ce type de problème à l'import ou il y en a d'autres ? A condition que le nombre de ces blocages reste exceptionnel, pour l'instant je ne vois pas de meilleure solution que ta proposition (notes ajoutées dans la description). Je vais créer des tickets pour passer les différents champs enum sous la même implé que couv geo et licence. On verra quand on pourra s'en occuper en fonction du reste et de la priorisation. J'attends un avis de @DaFrenchFrog en tout cas en ce qui me concerne tu peux lancer l'import en prod |
Pour info il y avait quelques coquilles que j'ai corrigées dans le fichier (qui du coup n'était plus valide, je sais pas ce qui s'est passé à un moment dans les dernières semaines). Ca ne règle pas le problème d'enum que tu pointes mais ça peut pas faire de mal à l'import. |
Ça concerne |
Les formats sont libres non ? On devait passer ce champ en texte simple avec suggestion des termes existant je crois (je crois aussi que je devais faire un ticket à ce propos mais il est peut-être passé à la trappe 😥 ).
Cette solution me semble bien et cela permettra de retrouver facilement les fdd pour les corriger. |
@DaFrenchFrog Peut-être. En tout cas pour l'instant les maquettes correspondent à ce qu'on a implémenté. Mais je crois bien qu'on en avait parlé de temps en temps, mais que malheureusement on ne s'est jamais mis ça dans le backlog... |
J'ai créé un début de ticket ici #477 |
@DaFrenchFrog Super est-ce que tu peux faire une issue pour chacun des champs concernés en détaillant l'implé avec les différents changements éventuels (contribution fiche, lecture fiche et recherche) ? Sinon je m'en occuperais plus tard. |
@florimondmanca Du coup on est d'accord, tu peux faire l'import (avec le fichier mis à jour si possible) cette fois en prod. |
@johanricher Je pense que pour |
Une implé avec des "garde fous" serait acceptable par les parties prenantes, comme pour les champs qui ont déjà ce systeme. |
Concernant la fréquence, si on considère que la question de garder une liste de valeurs contrôlée (enum donc) vaut la peine d'être approfondie, c'est probablement avec des arguments en faveur de l'interopérabilité et donc de l'ordre (cathédrale vs bazar). Auquel cas il faudrait à mon sens s'appuyer sur un standard international. En l'occurence DCAT fait la recommandation suivante :
Mais "quinquennal" n'y est pas. 😆 |
Dans ce cas je pense que c'est un sujet dont il faut lui parler. Si nous sommes dans un démarche d'accompagnement je pense qu'il faut indiquer qu'à terme nous allons nous diriger vers le modèle enum pour qu'ils puissent préparer leur transition. |
Concernant le champ fréquence, l'enum est déjà ce qui est implémenté : https://github.com/etalab/catalogage-donnees/blob/master/client/src/constants.ts#L30 Le problème c'est que cette liste est déjà considérée comme incomplète et le sera toujours malheureusement, même avec une liste plus complète comme celle de Dublin Core. Moi je suis favorable à passer tous les champs en string sans contrainte d'enum mais avec une implé plus fonctionnelle qu'aujourd'hui (autocomplétion, tri par popularité, multi select quand c'est applicable, etc.). |
L'enum permet un meilleur contrôle lors de la recherche et d'éviter des redondance... Pourquoi ne pas suivre les recommandation DCAT ? Au moins on ne prend pas de responsabilité sur les choix qui sont fait et on est plus dans les standards ? |
Ok mais le "quinquennal" du MC on le met où ? et le "quadriennal" de l'organisation X ? et le "deux fois par semaine" de l'organisation Y ? |
Ce sont les contraintes du standard ? 🤷♂️ |
@johanricher Le catalogue corrigé a été importé sur prod. https://catalogue.data.gouv.fr/ Tu peux vérifier avec le compte admin Si OK, ce ticket pourra être fermé. :) |
C'est bon ! 🎉 |
@johanricher je peux envoyer un mail à RA pour vérifier qu'elle peut se connecter et consulter le catalogue ? |
Oui tous les feux sont au vert pour faire la validation avec elle. Tous les critères d'acceptation doivent absolument être vérifiés avec elle sur ces 2 tickets : Tant que la vérification n'est pas faite, on ne fermera pas les tickets (donc c'est bloquant pour la livraison). |
A la suite de
Le fichier à importer (conforme au schéma du MC) est
catalogue_mc_20220921.csv
dans le dossier/Parties prenantes/MC
sur Nextcloud.The text was updated successfully, but these errors were encountered: