Skip to content
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

Add experimental import tool and documentation #504

Merged
merged 5 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/fr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ Table des matières :

- [Démarrage](./demarrage.md) - Un guide pour vous mettre en route.
- [Outils de développement](./outils.md) - Description des outils de développement à disposition.
- [Outils de gestion des organisations et catalogues](./outils-orgas.md) - Description de l'outillage permettant de gérer les organisations et catalogues enregistrés dans l'outil (dépôt de config, import, etc).
- [Opérations](./ops.md) - Un guide concernant l'infrastructure, le déploiement, les environnements, etc.
- [Trucs et astuces](./trucs-et-astuces.md) - Quelques "conseils de pro" qui peuvent vous simplifier la vie.
87 changes: 87 additions & 0 deletions docs/fr/outils-orgas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Outils de gestion des organisations et catalogues

## Dépôt de configuration

_Aussi appelé "repo de config"_

Lien GitHub : [etalab/catalogage-donnees-config](https://github.com/etalab/catalogage-donnees-config)

Ce dépôt sert à stocker les organisations enregistrées sur l'instance de production [catalogue.data.gouv.fr](https://catalogue.data.gouv.fr) ainsi que leur catalogue au format [TableSchema](https://specs.frictionlessdata.io/table-schema/).

Pour en savoir plus sur son utilisation, consulter le README du dépôt de configuration.

## Importer un catalogue

Un outil expérimental _[lire : testé sommairement, et à modifier ou adapter selon les besoins]_ est disponible pour importer un catalogue.

### Utilisation

L'organisation et le catalogue doivent avoir été créés au préalable.

1. Écrire un fichier de configuration pour l'import à partir de l'exemple [import.config.example.yml](https://github.com/etalab/catalogage-donnees/blob/master/tools/import.config.example.yml).

1. Générer un fichier d'initdata grâce à l'outil :

```bash
python -m tools.import_catalog <CONFIG_PATH> <OUT_PATH>
```

Où `<CONFIG_PATH>` est le chemin du fichier de configuration écrit à l'instant, et `<OUT_PATH>` un chemin de sortie pour le fichier d'initdata résultant.

1. Tester l'import en local :

* Partir d'une base de données vierge. Par exemple :

```
dropdb catalogage
createdb catalogage
make migrate
```

* Importer les organisations et catalogues à partir du dépôt de configuration. Pour ce faire :
* Dans le `.env`, définir :
```dotenv
APP_CONFIG_REPO_API_KEY=abcd1234 # N'importe quelle valeur
```
* Démarrer le serveur d'API local avec `make serve`.
* Dans le `.env` du dépôt de configuration, définir :
```dotenv
CATALOGAGE_API_URL=abcd1234 # La même valeur que côté catalogage-donnees
```
Puis lancer `make upload`.

* Lancer l'initdata :

```bash
python -m tools.initdata <OUT_PATH>
```

* Démarrer le serveur et vérifier que l'import s'est correctement déroulé.

1. Si et seulement si le comportement local est validé, procéder à l'import en production :

* Remplacer temporairement (sans faire de commit) le fichier d'initdata de la prod `ops/ansible/prod/assets/initdata.yml` par le fichier d'initdata généré.
* Lancer `make ops-initdata env=prod`.
* Remettre le fichier d'initdata de la prod dans son état d'origine (le git diff doit être vierge).
* Vérifier le comportement en production.

### Options notables

* _(Requis)_ `organization_siret` - `str`

SIRET de l'organisation dont il faut peupler le catalogue.

* _(Requis)_ `input_file.path` - `str`

Chemin vers le fichier CSV contenant le catalogue.

Les colonnes du CSV doivent refléter le schéma du catalogue tel que défini dans le dépôt de configuration :

* Champs du schéma communs (`titre`, `description`, etc) ;
* Champs complémentaires.

Les colonnes ne correspondant ni au schéma commun, ni aux champs complémentaires, doivent être supprimées du fichier CSV ou, à défaut, explicitement indiquées dans `ignore_fields` (voir ci-dessous). Toute incohérence avec les champs complémentaires du catalogue en base de données entraînera une erreur.

* `ignore_fields` - `List[str]`

La liste des colonnes ne correspondant ni au schéma commun, ni aux champs complémentaires.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ addopts =
-rxXs
--cov=server
--cov=tests
--cov=tools
--cov-report=term-missing
--cov-config=.coveragerc
--cov-fail-under=90
Expand Down
93 changes: 93 additions & 0 deletions tests/tools/test_import_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from pathlib import Path
from textwrap import dedent

import pytest
import yaml

from server.application.catalogs.commands import CreateCatalog
from server.config.di import resolve
from server.domain.catalogs.entities import BoolExtraField
from server.seedwork.application.messages import MessageBus
from tools import import_catalog

from ..factories import CreateOrganizationFactory, CreateTagFactory


@pytest.mark.asyncio
async def test_import_catalog_example(tmp_path: Path) -> None:
csv_path = tmp_path / "catalog.csv"
example_config_path = Path() / "tools" / "import.config.example.yml"
out_path = tmp_path / "initdata.yml"

bus = resolve(MessageBus)

# Simulate an existing organization and catalog
siret = await bus.execute(
CreateOrganizationFactory.build(name="Ministère 1", siret="11004601800013")
)
await bus.execute(
CreateCatalog(
organization_siret=siret,
extra_fields=[
BoolExtraField(
organization_siret=siret,
name="donnees_geoloc",
title="Données géolocalisées",
hint_text="Les données sont-elles géolocalisées ?",
data={"true_value": "Oui", "false_value": "Non"},
)
],
)
)

# Simulate a pre-existing tag.
await bus.execute(CreateTagFactory.build(name="Tag 1"))

csv_path.write_text(
dedent(
"""\
titre;description;mots_cles;nom_orga;siret_orga;id_alt_orga;service;si;contact_service;contact_personne;date_pub;date_maj;freq_maj;couv_geo;url;format;licence;donnees_geoloc
Titre1;Description1;"Tag 1,Tag 2";Ministère 1;11004601800013;;Direction1;SI1;[email protected];[email protected];;2022-10-06;annuelle;aquitaine;;geojson, xls, oracle et shp;etalab-2.0;oui
Titre2;Description2;"Tag 1,Tag 3";Ministère 1;11004601800013;;Direction1;SI2;[email protected];[email protected];;;Invalid;NSP;;Information manquante;etalab-2.0;oui
""" # noqa
)
)

config_path = tmp_path / "catalogue.csv"
config_path.write_text(
example_config_path.read_text().replace("REPLACE_ME", str(csv_path))
)

code = await import_catalog.main(config_path, out_path)
assert code == 0

with out_path.open() as f:
initdata = yaml.safe_load(f)

assert not initdata["organizations"]
assert not initdata["catalogs"]
assert not initdata["users"]
assert [t["params"] for t in initdata["tags"]] == [
# "Tag 1" already existed
{"name": "Tag 2"},
{"name": "Tag 3"},
]

assert len(initdata["datasets"]) == 2

assert all(d["params"]["organization_siret"] == siret for d in initdata["datasets"])

d0 = initdata["datasets"][0]["params"]
assert sorted(d0["formats"]) == ["database", "file_gis", "file_tabular"]
assert d0["geographical_coverage"] == "aquitaine"
assert d0["update_frequency"] == "yearly"
assert "[[ Notes d'import automatique ]]" not in d0["description"]

d1 = initdata["datasets"][1]["params"]
assert d1["geographical_coverage"] == "(Information manquante)"
assert d1["formats"] == ["other"]
assert d1["update_frequency"] is None
assert d1["url"] is None
assert "[[ Notes d'import automatique ]]" in d1["description"]
assert "Format : (Information manquante)" in d1["description"]
assert "Fréquence de mise à jour (valeur originale) : Invalid" in d1["description"]
30 changes: 30 additions & 0 deletions tools/import.config.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
input_csv:
path: REPLACE_ME
encoding: utf-8
delimiter: ";"
na_values:
- "Information manquante"
- "NSP"

organization_siret: "11004601800013"

ignore_fields:
- id_alt_orga
- date_pub

formats:
list_map:
"oracle et shp": [database, file_gis]

map:
"xls": file_tabular
"geojson": file_gis

update_frequency:
map:
"aucune": never
"en continu": realtime
"annuelle": yearly

last_updated_at:
format: "%Y-%M-%d" # See: https://docs.python.org/fr/3/library/datetime.html#strftime-and-strptime-format-codes
Loading