API GeoJSON
Cette page décrit l’API ajoutée dans api/, ce qu’elle coûte en temps de réponse, sa mise en production, et la suppression des GeoJSON du pipeline qu’elle a permise. L’API est déployée par Argo CD sur https://cartiflette-api.lab.sspcloud.fr, et le pipeline ne produit plus que du GeoParquet.
Pourquoi une API
Le pipeline publiait chaque niveau sous deux formes :
- en GeoParquet, un fichier par niveau (32 jobs par millésime), que le client Python filtre à la lecture ;
- en GeoJSON, un fichier par valeur de découpage (
DEPARTEMENT=75,REGION=11…), soit 74 jobs par millésime, chacun avec son pod Argo, ses relances et ses fichiers à écrire sur S3.
Le GeoJSON n’existait que pour les clients qui ne savent pas lire le GeoParquet (R, JS/Observable, navigateur, client Python antérieur à 0.2.0). L’API le sert à partir du GeoParquet, à la demande : le pipeline n’a plus besoin de produire les 74 jobs GeoJSON.
Avant client R / JS ──HTTPS──▶ S3 : un GeoJSON par valeur (74 jobs)
Maintenant client R / JS ──HTTPS──▶ API ──HTTPS──▶ S3 : GeoParquet (32 jobs)
Ce qui a été ajouté
| Élément | Fichier | Rôle |
|---|---|---|
| API | api/cartiflette_api/app.py |
application FastAPI, paquet cartiflette-api (non publié) |
| Tests unitaires | api/tests/test_api.py |
18 tests sur un stockage local simulé, sans réseau |
| Mesures | api/benchmark.py, api/benchmark-2025.json |
comparaison API / fichiers sur les cas d’usage du site |
| Documentation | api/README.md, cette page |
|
| Tests d’intégration | python-package/cartiflette/tests/test_integration.py |
peuvent passer par l’API avec CARTIFLETTE_API_URL |
| Image Docker | api/Dockerfile, .dockerignore |
image de l’API, à construire depuis la racine du dépôt ; le .dockerignore racine, commun aux deux images, liste les fichiers autorisés |
| CI | .github/workflows/check.yml, .github/workflows/docker.yml |
jobs api : tests unitaires à chaque push, construction et publication de l’image |
Le client Python et le pipeline ne changent pas.
Vérifier
cd api && uv run pytest tests # sans réseau
cd api && uv run uvicorn --factory cartiflette_api.app:create_app --port 8000
# dans un autre terminal : les cas d'usage du site, via l'API
cd python-package/cartiflette
CARTIFLETTE_API_URL=http://localhost:8000 uv run pytest -m integrationVia l’API, les tests d’intégration donnent 91 réussis, 5 ignorés et 2 échecs attendus (les mêmes que sans API). Les 5 ignorés testent des options propres au client Python (force, engine="duckdb", topojson).
Ce que coûte l’API
api/benchmark.py rejoue les cas d’usage des tests d’intégration (millésime 2025, test/v0.2.0) et compare le GeoJSON obtenu via l’API au GeoJSON lu directement sur S3. Il lance l’API lui-même et la redémarre avant chaque cas : les chiffres ci-dessous sont ceux d’une première requête, cache vide.
Mesures depuis un pod du SSPCloud, donc dans le même réseau que MinIO :
| Cas | Taille | Fichiers | API |
|---|---|---|---|
| Communes d’un département | 0,4 Mo | 0,03 s | 0,22 s |
| Communes de deux départements | 2 Mo | 0,03 s | 0,28 s |
| Départements, DROM rapprochés | 15 Mo | 0,10 s | 0,69 s |
| Régions, France entière | 6 Mo | 0,05 s | 0,43 s |
| Bassins de vie, France entière, sans simplification | 50 Mo | 0,25 s | 2,05 s |
Dans le même réseau, l’API ajoute donc environ 0,2 s pour une requête courante et environ 2 s pour les plus gros niveaux. Sur ces gros niveaux, le temps se répartit entre la lecture (~0,3 s), la conversion en GeoJSON (~0,5 s) et la compression (~0,8 s).
Pour un utilisateur hors du SSPCloud, c’est le téléchargement qui domine. MinIO envoie les fichiers GeoJSON sans compression, l’API en gzip, soit trois fois moins d’octets. Estimation à 20 Mbit/s :
| Cas | Fichiers | API |
|---|---|---|
| Communes d’un département | 0,2 s | 0,3 s |
| Départements, DROM rapprochés | 6 s | 3 s |
| Bassins de vie, France entière, sans simplification | 20 s | 10 s |
L’API est donc un peu plus lente pour les petites requêtes et plus rapide pour les grosses. Ce second gain vient de la compression, pas de l’API elle-même.
Pour le client Python, rien ne change : lire le GeoParquet directement reste le plus rapide (1,6 s contre environ 5 s via l’API pour obtenir un GeoDataFrame des bassins de vie). L’API sert les autres clients.
Le détail des mesures est dans api/README.md.
Mise en production : les enjeux
L’API est déployée (étape 1) et le pipeline ne produit plus de GeoJSON (étape 4). Les clients R et JS doivent donc passer par l’API (étape 3) : sinon, ils ne trouveront aucun millésime publié après ce changement.
1. Déployer l’API
L’image Docker existe : api/Dockerfile, construite à la racine du dépôt (l’API dépend du client dans python-package/) :
docker build -f api/Dockerfile -t cartiflette-api .
docker run -p 8000:8000 cartiflette-apiLa CI (.github/workflows/docker.yml, job api) la construit à chaque PR et la publie sur main et sur les tags v* et api-* sous inseefrlab/cartiflette-api:v<version>, la version étant celle de api/pyproject.toml (pas le nom du tag). Un tag api-* (ex. api-0.1.0) ne construit que l’image de l’API : ni l’image du pipeline ni le client PyPI, réservés aux tags v*. Comme pour l’image du pipeline, pousser sur main sans monter cette version écrase l’image déjà déployée.
L’image écoute sur le port 8000, sans utilisateur root, avec les extensions DuckDB préinstallées. Un seul processus par conteneur : DuckDB utilise déjà tous les cœurs, on ajoute de la capacité en ajoutant des réplicas. Aucun secret n’est nécessaire : l’API lit les fichiers publics en HTTPS.
Les manifestes sont prêts, sur le modèle GitOps du cours ENSAE reproductibilité :
| Fichier | Contenu |
|---|---|
api/deployment/deployment.yaml |
2 réplicas de inseefrlab/cartiflette-api:v0.1.2 ; 0,5 à 2 CPU, 1 à 2 Gio ; sondes sur /health |
api/deployment/service.yaml |
port 80 vers le port 8000 des pods |
api/deployment/ingress.yaml |
https://cartiflette-api.lab.sspcloud.fr, CORS ouvert en lecture (GET) pour les navigateurs |
api/application.yaml |
application Argo CD : suit api/deployment/ sur main, namespace projet-cartiflette, synchronisation automatique |
Contrairement au cours, les manifestes restent dans ce dépôt plutôt que dans un dépôt GitOps séparé : Argo CD lit le sous-dossier api/deployment. application.yaml est volontairement en dehors de ce dossier, pour qu’Argo CD ne l’applique pas lui-même.
DuckDB suit les limites du conteneur : autant de threads que de CPU autorisés, et 80 % de la limite mémoire. Changer les limites change donc directement sa capacité.
Pour déployer :
- lancer un service Argo CD dans le projet
cartiflettedu SSPCloud (Mes services, catalogue Automation) ; - New App, puis Edit as YAML : coller
api/application.yaml, puis Create ; - vérifier
https://cartiflette-api.lab.sspcloud.fr/health.
Pour une nouvelle version : monter la version dans api/pyproject.toml, laisser la CI publier l’image (push sur main ou tag api-*), puis changer le tag de l’image dans deployment.yaml. Le commit sur main suffit : Argo CD redéploie. Le tag de l’image est fixe et non latest, pour savoir ce qui tourne et pouvoir revenir en arrière en annulant le commit.
Restent à décider :
- l’adresse publique :
cartiflette-api.lab.sspcloud.frest libre aujourd’hui, mais c’est elle que les clients R et JS inscriront dans leur code, elle ne doit plus changer ensuite ; - un responsable du service.
2. Accepter de passer de fichiers à un service
C’est le vrai changement de nature. Aujourd’hui, les clients lisent des fichiers statiques sur MinIO : tant que MinIO fonctionne, cartiflette fonctionne. Avec l’API, il y a un service à faire tourner :
- disponibilité : si l’API tombe, les clients R et JS n’ont plus de GeoJSON pour les millésimes qui n’existent qu’en GeoParquet. Il faut au moins deux réplicas et une surveillance ;
- charge : chaque requête coûte du calcul (environ 2 s de CPU pour un niveau France entière non simplifié), là où un fichier statique ne coûte rien à servir. Il n’y a pas de cache des réponses pour l’instant. Les requêtes les plus fréquentes (France entière, départements) sont peu nombreuses et toujours identiques : un cache serait le premier levier si la charge monte ;
- abus : l’API est publique ; prévoir une limite de débit si besoin.
3. Faire passer les clients par l’API
| Client | Ce qu’il faut changer |
|---|---|
| R, JS/Observable, site web | remplacer https://minio.lab.sspcloud.fr par l’adresse de l’API ; les chemins restent les mêmes |
| Python ≥ 0.2.0 | rien : il lit le GeoParquet directement |
| Python < 0.2.0 | impossible à modifier : il lit les fichiers GeoJSON sur MinIO et ne trouvera pas les millésimes produits sans GeoJSON |
Le dernier cas est le coût inévitable. Les utilisateurs d’anciennes versions du client Python devront mettre à jour pour les nouveaux millésimes, et continueront de lire les anciens millésimes sans rien changer.
4. Arrêter de produire les GeoJSON (fait)
Le workflow Argo n’a plus d’étape split : list-jobs ne liste que les 32 jobs GeoParquet, et le code correspondant (combinations, process, split_and_upload, mapshaper.split, paths.create_path_bucket) est supprimé. Le pipeline passe de 106 jobs par millésime (74 + 32) à 32.
Rien n’est supprimé sur S3 : les GeoJSON déjà publiés (2022 en production) restent lus par les anciens clients et par l’API, qui y redirige. Les supprimer serait une décision distincte.
Conséquences :
- les millésimes publiés désormais n’existent qu’en GeoParquet : lisibles par le client Python ≥ 0.2.0 et par l’API, pas par les clients qui lisent les GeoJSON sur MinIO (R et JS non migrés, Python < 0.2.0) ;
carti_download(..., vectorfile_format="geojson", force=True)lit le GeoParquet pour ces millésimes, avec un avertissement : il n’y a plus de fichier GeoJSON à forcer (client 0.2.1,geojson_available) ;- republier 2022 n’écrase plus ses GeoJSON : cela ajoute un GeoParquet à côté (voir 02).
Décisions à prendre
- Déployer l’API, où, avec quelle adresse et qui en est responsable.
- Accepter qu’un service remplace des fichiers statiques, avec sa surveillance.
- Calendrier de migration des clients R et JS.
- Comment prévenir les utilisateurs du client Python < 0.2.0, qui ne verront pas les nouveaux millésimes.
Comment elle fonctionne
L’API dépend du client Python (installé depuis
python-package/cartiflette) et réutilise sa fonctionread_parquet. Elle ne connaît donc pas les chemins S3 par elle-même : il n’y a toujours que deux implémentations du contrat (pipeline et client), pas trois.Pour chaque requête :
ST_AsGeoJSON) ;Deux routes
/v1/geojson, avec les paramètres decarti_downloadet plusieurs valeurs dans une seule réponse :Les chemins des fichiers GeoJSON actuels, pour qu’un client existant n’ait qu’à changer d’hôte :
Millésimes sans GeoParquet (2022)
Le client n’a pas à distinguer les millésimes. Quand il n’y a pas de GeoParquet :
/v1/geojsonavec plusieurs valeurs lit les fichiers correspondants et les renvoie fusionnés en une seule réponse, puisqu’une redirection ne peut viser qu’un fichier.L’existence du GeoParquet est vérifiée une fois puis gardée en mémoire : si un GeoParquet est publié plus tard pour 2022, redémarrer l’API pour qu’elle le lise.
Différences avec les fichiers
Le contenu est équivalent, pas identique à l’octet :
AREA(territoire) ;Erreurs : 404 si un fichier n’existe pas, si le niveau ne peut pas être filtré de cette façon ou si une valeur est introuvable ; 400 pour un
path_within_bucketinvalide.