Compare commits

...

11 Commits

20 changed files with 1024 additions and 2 deletions

52
.env.template Normal file
View File

@ -0,0 +1,52 @@
#
# Docker
#
#COMPOSE_FILE=docker-compose.yml:docker-compose.traefik.yml
COMPOSE_FILE=docker-compose.yml:docker-compose.local.yml
OPENDATA_NETWORK_NAME=opendata
#
# PostgreSQL
#
POSTGRES_CONTAINER_NAME=postgres
POSTGRES_IMAGE=postgres:16.1
POSTGRES_VOLUME_NAME=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=utiliser-la-commande-pour-generer-un-mot-de-passe # tr -cd '[:alnum:]' < /dev/urandom | fold -w "64" | head -n 1 | tr -d '\n' ; echo
POSTGRES_DB=postgres
DB_ANON_ROLE=anon
DB_SCHEMA=public
#
# PostgREST
#
POSTGREST_DOMAIN=localhost
POSTGREST_CONTAINER_NAME=postgrest
POSTGREST_IMAGE=postgrest/postgrest:v12.0.2
POSTGREST_URL=http://${POSTGREST_DOMAIN}:3000/
PGRST_DB_MAX_ROWS=100
#
# Swagger UI
#
SWAGGER_DOMAIN=ui.opendata.example.org
SWAGGER_CONTAINER_NAME=swagger
SWAGGER_IMAGE=swaggerapi/swagger-ui:v5.11.0
#
# Traefik
#
TRAEFIK_NETWORK_NAME=traefik
TRAEFIK_ROUTER_NAME=opendata
TRAEFIK_ENTRYPOINTS=websecure

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
tmp
.env
initdb/*.csv

View File

@ -1,9 +1,76 @@
# Data Project
# Project Open Data
Création d'une API unifié pour accéder à l'ensemble des données dont P4Pillon a besoin.
## Composants logiciels
Le principe du projet est de pouvoir intérroger les données de l'open data depuis un réseau local et ainsi s'affranchir des pannes d'internet ou des APIs. Les données pourront être mise à jour régulièrement pour permettre d'avoir les dernières mise à jour.
## Liens
- [Source du projet](https://forge.p4pillon.org/P4Pillon/data)
- [Gestion des tâches](https://forge.p4pillon.org/P4Pillon/OpenDataAPI/projects/3)
- [API de P4Pillon](https://data.p4pillon.org)
- [Interface Swagger pour l'API](https://ui.data.p4pillon.org)
- [Support et questionnement](https://matrix.to/#/!UXjiUOiWQMEsMgMNMN:converser.eu)
## Données
### ✅ FINESS
Informations sur les établissements sanitaires, sociaux, médico-sociaux, et de formation aux professions de ces secteurs.
Liens :
- [Site web](https://finess.esante.gouv.fr/)
- [Page web sur l'Open Data du gouvernement](https://www.data.gouv.fr/fr/datasets/finess-extraction-du-fichier-des-etablissements/)
### ✅ INPI
L'Institut national de la propriété industrielle, abrégé par le sigle INPI, est un établissement public à caractère administratif, placé sous la tutelle du ministère français de l'Économie, de l'Industrie et du Numérique.
Il y a 2 posibilités pour récupérer les données de l'ensemble des entreprises française :
- depuis l'API
- depuis [des fichiers](https://www.data.gouv.fr/fr/datasets/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/)
## Composants logiciels du projet
- Base de données : [PostgreSQL](https://www.postgresql.org/)
- API : [PostgREST](https://postgrest.org/) ([Documentation](https://postgrest.org/en/stable/explanations/install.html#docker))
- UI : [Swagger UI](https://swagger.io/tools/swagger-ui/)
- Virtualisation : [Docker](https://docs.docker.com/)
- Orchestrateur : [Docker Compose](https://docs.docker.com/compose/)
## Script
Un script permet d'initialiser le projet en téléchargeant les dépendences, les données de l'Open Data ainsi que de transformer ses données en fichiers CSV.
Il faut avoir Docker d'installé et executer cette commande :
```bash
bash build.sh
```
### Docker Compose
Nous utilisons Docker Compose pour la mise en place des différents services.
Le projet propose 3 fichiers permettant une execution local ou sur un serveur ayant un serveur Traefik de configuré.
La configuration se fait à l'aide d'un fichier `.env`, vous devez modifier les valeurs du fichier `.env.template`.
Voici quelques commandes possibles :
```bash
docker compose --env-file .env up -d # Création des conteneurs
docker compose --env-file .env down -v # Suppresion des conteneurs ainsi que des données
```
### PostgreSQL
La fonction `COPY` est utilisé dans les fichiers SQL du dossier `./initdb` permettant d'importer les données d'un fichier CSV dans une base de données.
### PostgREST
- [Pour la configuration](https://postgrest.org/en/latest/configuration.html#environment-variables)
- [Pour requêter des données](https://postgrest.org/en/stable/references/api/tables_views.html#horizontal-filtering-rows)

25
build.sh Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# Commande de base avec Docker
npm="docker run -it --rm --user $(id -u):$(id -g) -v $PWD:/home/node/app -w /home/node/app node:lts-alpine ash -c 'npm config set update-notifier false && npm"
ash="docker run -it --rm --user $(id -u):$(id -g) -v $PWD:/home/node/app -w /home/node/app apteno/alpine-jq ash"
# Suppression des fichiers
echo $'\n\n#\n# Suppression des fichiers 🧹\n#\n'
eval $npm run clean\'
# Installation des dépendances
echo $'\n\n#\n# Installation des dépendances ⬇️\n#\n'
eval $npm install\'
# Téléchargement des données
echo $'\n\n#\n# Téléchargement des données 💡\n#\n'
eval $npm run download\'
eval $ash ./scripts/download.sh
# Transformation des données
echo $'\n\n#\n# Transformation des données 🏗️\n#\n'
eval $npm run transform\'
eval $ash ./scripts/transform.sh
echo $'\n\n#\n# Le projet est prêt à être lancé avec Docker 🚀\n#\n'

17
docker-compose.local.yml Normal file
View File

@ -0,0 +1,17 @@
---
version: "3.8"
services:
postgres:
ports:
- "5432:5432"
postgrest:
ports:
- "3000:3000"
swagger:
ports:
- "8080:8080"

View File

@ -0,0 +1,31 @@
---
version: "3.8"
networks:
traefik:
name: ${TRAEFIK_NETWORK_NAME:-traefik}
external: true
services:
postgrest:
networks:
- traefik
labels:
- traefik.enable=true
- traefik.docker.network=${TRAEFIK_NETWORK_NAME:-traefik}
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_postgrest.rule=Host(`${POSTGREST_DOMAIN:?err}`)
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_postgrest.entrypoints=${TRAEFIK_ENTRYPOINTS:-websecure}
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_postgrest.tls.certResolver=letsencrypt
swagger:
networks:
- traefik
labels:
- traefik.enable=true
- traefik.docker.network=${TRAEFIK_NETWORK_NAME:-traefik}
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_swagger.rule=Host(`${SWAGGER_DOMAIN:?err}`)
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_swagger.entrypoints=${TRAEFIK_ENTRYPOINTS:-websecure}
- traefik.http.routers.${TRAEFIK_ROUTER_NAME:-opendata}_swagger.tls.certResolver=letsencrypt
- traefik.http.services.${TRAEFIK_ROUTER_NAME:-opendata}_swagger.loadbalancer.server.port=8080

59
docker-compose.yml Normal file
View File

@ -0,0 +1,59 @@
---
version: "3.8"
networks:
opendata:
name: ${OPENDATA_NETWORK_NAME:-opendata}
volumes:
postgres:
name: ${POSTGRES_VOLUME_NAME:-postgres}
services:
postgres:
container_name: ${POSTGRES_CONTAINER_NAME:-postgres}
image: ${POSTGRES_IMAGE:-postgres:16.1-alpine}
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:?err}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?err}
POSTGRES_DB: ${POSTGRES_DB:?err}
DB_ANON_ROLE: ${DB_ANON_ROLE:?err}
DB_SCHEMA: ${DB_SCHEMA:?err}
volumes:
- postgres:/var/lib/postgresql/data
- "./initdb:/docker-entrypoint-initdb.d"
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- opendata
postgrest:
container_name: ${POSTGREST_CONTAINER_NAME:-postgrest}
image: ${POSTGREST_IMAGE:-postgrest/postgrest:v12.0.0}
restart: always
# https://postgrest.org/en/latest/configuration.html#environment-variables
environment:
PGRST_DB_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:?err}
PGRST_DB_SCHEMA: ${DB_SCHEMA}
PGRST_DB_ANON_ROLE: ${DB_ANON_ROLE}
PGRST_OPENAPI_SERVER_PROXY_URI: ${POSTGREST_URL:-http://localhost:3000}
# https://postgrest.org/en/stable/references/configuration.html#db-max-rows
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-100}
depends_on:
- postgres
networks:
- opendata
swagger:
container_name: ${SWAGGER_CONTAINER_NAME:-swagger}
image: ${SWAGGER_IMAGE:-swaggerapi/swagger-ui:v5.11.0}
restart: always
environment:
API_URL: ${POSTGREST_URL:-http://localhost:3000/}
depends_on:
- postgrest
networks:
- opendata

View File

@ -0,0 +1,9 @@
#!/bin/bash
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-END
CREATE USER ${DB_ANON_ROLE};
GRANT USAGE ON SCHEMA ${DB_SCHEMA} TO ${DB_ANON_ROLE};
ALTER DEFAULT PRIVILEGES IN SCHEMA ${DB_SCHEMA} GRANT SELECT ON TABLES TO ${DB_ANON_ROLE};
GRANT SELECT ON ALL SEQUENCES IN SCHEMA ${DB_SCHEMA} TO ${DB_ANON_ROLE};
GRANT SELECT ON ALL TABLES IN SCHEMA ${DB_SCHEMA} TO ${DB_ANON_ROLE};
END

46
initdb/2_finess.sql Normal file
View File

@ -0,0 +1,46 @@
BEGIN;
CREATE TABLE IF NOT EXISTS finess (
nofinesset VARCHAR(255),
nofinessej VARCHAR(255),
rs VARCHAR(255),
rslongue VARCHAR(255),
complrs VARCHAR(255),
compldistrib VARCHAR(255),
numvoie VARCHAR(255),
typvoie VARCHAR(255),
voie VARCHAR(255),
compvoie VARCHAR(255),
lieuditbp VARCHAR(255),
commune VARCHAR(255),
departement VARCHAR(255),
libdepartement VARCHAR(255),
ligneacheminement VARCHAR(255),
telephone VARCHAR(255),
telecopie VARCHAR(255),
categetab VARCHAR(255),
libcategetab VARCHAR(255),
categagretab VARCHAR(255),
libcategagretab VARCHAR(255),
siret VARCHAR(255),
codeape VARCHAR(255),
codemft VARCHAR(255),
libmft VARCHAR(255),
codesph VARCHAR(255),
libsph VARCHAR(255),
dateouv DATE DEFAULT '1900-01-01',
dateautor DATE DEFAULT '1900-01-01',
datemaj DATE DEFAULT '1900-01-01',
numuai VARCHAR(255),
coordxet FLOAT DEFAULT 0,
coordyet FLOAT DEFAULT 0
);
COPY finess FROM '/docker-entrypoint-initdb.d/finess.csv' DELIMITER ';' CSV HEADER;
ALTER TABLE ONLY finess
ADD CONSTRAINT finess_pkey PRIMARY KEY (nofinesset);
COMMIT;
ANALYZE finess;

View File

@ -0,0 +1,18 @@
BEGIN;
CREATE TABLE IF NOT EXISTS regions (
code VARCHAR(3),
chefLieu VARCHAR(5),
nom VARCHAR(255),
typeLiaison VARCHAR(255),
region_zone VARCHAR(255)
);
COPY regions FROM '/docker-entrypoint-initdb.d/regions.csv' DELIMITER ',' CSV;
ALTER TABLE ONLY regions
ADD CONSTRAINT regions_pkey PRIMARY KEY (code);
COMMIT;
ANALYZE regions;

77
initdb/4_sirene.sql Normal file
View File

@ -0,0 +1,77 @@
BEGIN;
CREATE TABLE IF NOT EXISTS sirene_stock_etablissement (
siren VARCHAR(9),
nic VARCHAR(5),
siret VARCHAR(14),
statutDiffusionEtablissement VARCHAR(1),
dateCreationEtablissement VARCHAR(19),
trancheEffectifsEtablissement VARCHAR(2),
anneeEffectifsEtablissement VARCHAR(19),
activitePrincipaleRegistreMetiersEtablissement VARCHAR(6),
dateDernierTraitementEtablissement VARCHAR(19),
etablissementSiege VARCHAR(5),
nombrePeriodesEtablissement NUMERIC(2),
complementAdresseEtablissement VARCHAR(38),
numeroVoieEtablissement VARCHAR(4),
indiceRepetitionEtablissement VARCHAR(4),
typeVoieEtablissement VARCHAR(4),
libelleVoieEtablissement VARCHAR(100),
codePostalEtablissement VARCHAR(5),
libelleCommuneEtablissement VARCHAR(100),
libelleCommuneEtrangerEtablissement VARCHAR(100),
distributionSpecialeEtablissement VARCHAR(26),
codeCommuneEtablissement VARCHAR(5),
codeCedexEtablissement VARCHAR(9),
libelleCedexEtablissement VARCHAR(100),
codePaysEtrangerEtablissement VARCHAR(5),
libellePaysEtrangerEtablissement VARCHAR(100),
complementAdresse2Etablissement VARCHAR(38),
numeroVoie2Etablissement VARCHAR(4),
indiceRepetition2Etablissement VARCHAR(4),
typeVoie2Etablissement VARCHAR(4),
libelleVoie2Etablissement VARCHAR(100),
codePostal2Etablissement VARCHAR(5),
libelleCommune2Etablissement VARCHAR(100),
libelleCommuneEtranger2Etablissement VARCHAR(100),
distributionSpeciale2Etablissement VARCHAR(26),
codeCommune2Etablissement VARCHAR(5),
codeCedex2Etablissement VARCHAR(9),
libelleCedex2Etablissement VARCHAR(100),
codePaysEtranger2Etablissement VARCHAR(5),
libellePaysEtranger2Etablissement VARCHAR(100),
dateDebut VARCHAR(19),
etatAdministratifEtablissement VARCHAR(1),
enseigne1Etablissement VARCHAR(50),
enseigne2Etablissement VARCHAR(50),
enseigne3Etablissement VARCHAR(50),
denominationUsuelleEtablissement VARCHAR(100),
activitePrincipaleEtablissement VARCHAR(6),
nomenclatureActivitePrincipaleEtablissement VARCHAR(8),
caractereEmployeurEtablissement VARCHAR(1)
);
COPY sirene_stock_etablissement FROM '/docker-entrypoint-initdb.d/StockEtablissement_utf8.csv' DELIMITER ',' CSV HEADER;
-- Index sur la colonne siren
CREATE INDEX idx_siren ON sirene_stock_etablissement (siren);
-- Index sur la colonne siret
CREATE INDEX idx_siret ON sirene_stock_etablissement (siret);
-- Index sur la colonne codePostalEtablissement
CREATE INDEX idx_codePostalEtablissement ON sirene_stock_etablissement (codePostalEtablissement);
-- Index sur la colonne libelleCommuneEtablissement
CREATE INDEX idx_libelleCommuneEtablissement ON sirene_stock_etablissement (libelleCommuneEtablissement);
-- Index sur la colonne activitePrincipaleEtablissement
CREATE INDEX idx_activitePrincipaleEtablissement ON sirene_stock_etablissement (activitePrincipaleEtablissement);
-- ALTER TABLE ONLY sirene_stock_etablissement
-- ADD CONSTRAINT sirene_stock_etablissement_pkey PRIMARY KEY (siren);
COMMIT;
ANALYZE sirene_stock_etablissement;

194
package-lock.json generated Normal file
View File

@ -0,0 +1,194 @@
{
"name": "data",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@etalab/decoupage-administratif": "^3.1.1",
"csv-parse": "^5.5.3",
"pg": "^8.11.3",
"proj4": "^2.10.0"
}
},
"node_modules/@etalab/decoupage-administratif": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@etalab/decoupage-administratif/-/decoupage-administratif-3.1.1.tgz",
"integrity": "sha512-/1yqSlo6Xerdz1MN+rh57hDWcAam18gwSK5u4wFjT1glRUiN/o1A4IQe6pvB9JqGvHv6WbF5X7WyE+yxeMqLig==",
"engines": {
"node": ">= 10"
}
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/csv-parse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.3.tgz",
"integrity": "sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A=="
},
"node_modules/mgrs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
"integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA=="
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.2",
"pg-pool": "^3.6.1",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/proj4": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.10.0.tgz",
"integrity": "sha512-0eyB8h1PDoWxucnq88/EZqt7UZlvjhcfbXCcINpE7hqRN0iRPWE/4mXINGulNa/FAvK+Ie7F+l2OxH/0uKV36A==",
"dependencies": {
"mgrs": "1.0.0",
"wkt-parser": "^1.3.3"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/wkt-parser": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz",
"integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw=="
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
}
}
}

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "opendata",
"type": "module",
"scripts": {
"clean": "rm ./node_modules -rf && rm ./tmp -rf && rm ./initdb/*.csv -rf",
"download": "node scripts/download.js",
"transform": "node scripts/transform.js"
},
"dependencies": {
"@etalab/decoupage-administratif": "^3.1.1",
"csv-parse": "^5.5.3",
"pg": "^8.11.3",
"proj4": "^2.10.0"
}
}

7
scripts/download.js Normal file
View File

@ -0,0 +1,7 @@
import { downloadFiness } from './helpers/finess.js'
async function main() {
downloadFiness()
}
main()

4
scripts/download.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/ash
echo $'- Téléchargement des données INPI'
wget https://www.data.gouv.fr/fr/datasets/r/0651fb76-bcf3-4f6a-a38d-bc04fa708576 -O ./tmp/StockEtablissement_utf8.zip

197
scripts/helpers/finess.js Normal file
View File

@ -0,0 +1,197 @@
import fs from 'fs';
import readline from 'readline';
import { downloadJSON, downloadAndSaveFile, filtrerEtEnregistrerLignes, loadEnvFile, customSplit } from './functions.js'
import { transformToGPS } from './gps.js'
const columns = [
'nofinesset',
'nofinessej',
'rs',
'rslongue',
'complrs',
'compldistrib',
'numvoie',
'typvoie',
'voie',
'compvoie',
'lieuditbp',
'commune',
'departement',
'libdepartement',
'ligneacheminement',
'telephone',
'telecopie',
'categetab',
'libcategetab',
'categagretab',
'libcategagretab',
'siret',
'codeape',
'codemft',
'libmft',
'codesph',
'libsph',
'dateouv',
'dateautor',
'datemaj',
'numuai',
'coordxet',
'coordyet'
];
export const downloadFiness = async () => {
const MAIN_URL = 'https://www.data.gouv.fr/api/1/datasets/finess-extraction-du-fichier-des-etablissements/'
const FILENAME = './tmp/finess.csv'
const data = await downloadJSON(MAIN_URL);
const finessUrl = data.resources.find(r => r.type === "main" && r.title.includes('géolocalisés'))?.url
downloadAndSaveFile(finessUrl, FILENAME)
}
export const transformFiness = async () => {
const fichierEntree = './tmp/finess.csv';
const fichierStructures = './tmp/finess_structureet.csv';
const fichierGeolocalisation = './tmp/finess_geolocalisation.csv';
const fichierSortie = './initdb/finess.csv'
const conditionFiltre = (ligne) => ligne.includes('structureet');
await filtrerEtEnregistrerLignes(fichierEntree, fichierStructures, fichierGeolocalisation, conditionFiltre);
await fusionnerCoordonneesGPSStructures(fichierStructures, fichierGeolocalisation, fichierSortie);
}
// Fonction pour fusionner les coordonnées GPS dans le fichier CSV de structures
export async function fusionnerCoordonneesGPSStructures(fichierStructures, fichierCoordonnees, fichierSortie) {
try {
// Créer un flux de lecture pour le fichier de coordonnées
const lecteurCoordonnees = readline.createInterface({
input: fs.createReadStream(fichierCoordonnees),
crlfDelay: Infinity
});
// Créer un dictionnaire pour stocker les coordonnées par structureid
const coordonnees = {};
// Fonction pour traiter chaque ligne du fichier de coordonnées
const traiterLigneCoordonnees = (ligne) => {
// exemple de ligne
// geolocalisation;970300802;317351.6;571220.2;2,ATLASANTE,100,IGN,BD_ADRESSE,V2.2,UTM_N22;2024-01-08
const valeurs = ligne.split(';');
const structureid = valeurs[1];
const coordX = parseFloat(valeurs[2]);
const coordY = parseFloat(valeurs[3]);
const system = valeurs[4]
coordonnees[structureid] = { system, coordX, coordY };
};
// Lire chaque ligne du fichier de coordonnées
for await (const ligne of lecteurCoordonnees) {
traiterLigneCoordonnees(ligne);
}
// Créer un flux de lecture pour le fichier de structures
const lecteurStructures = readline.createInterface({
input: fs.createReadStream(fichierStructures),
crlfDelay: Infinity
});
// Créer un flux d'écriture vers le fichier de sortie
const fichierSortieStream = fs.createWriteStream(fichierSortie);
// Création de l'entête d'insertion
// fichierSortieStream.write('INSERT INTO finess (' + columns.join(', ') + ') VALUES\n');
fichierSortieStream.write(columns.join(";") + '\n');
const ecrireLigneStructure = (ligne, isFirst) => {
let dataArray = ligne.split(';');
// Fix le nom d'une association contenant un ; ... "ASSO; LA RELÈVE"
if (dataArray.length > 34) {
dataArray = customSplit(ligne, ';')
dataArray = dataArray.map(value => value.replaceAll(";", "%3B"))
}
dataArray.shift(); // Suppression du premier champs inutil
// const defaultValue = (value) => (value === null || value === '' ? 'NULL' : `'${value.replaceAll("'", "''")}'`)
// const typedData = {
// nofinesset: defaultValue(dataArray[0]),
// nofinessej: defaultValue(dataArray[1]),
// rs: defaultValue(dataArray[2]),
// rslongue: defaultValue(dataArray[3]),
// complrs: defaultValue(dataArray[4]),
// compldistrib: defaultValue(dataArray[5]),
// numvoie: defaultValue(dataArray[6]),
// typvoie: defaultValue(dataArray[7]),
// voie: defaultValue(dataArray[8]),
// compvoie: defaultValue(dataArray[9]),
// lieuditbp: defaultValue(dataArray[10]),
// commune: defaultValue(dataArray[11]),
// departement: defaultValue(dataArray[12]),
// libdepartement: defaultValue(dataArray[13]),
// ligneacheminement: defaultValue(dataArray[14]),
// telephone: defaultValue(dataArray[15]),
// telecopie: defaultValue(dataArray[16]),
// categetab: defaultValue(dataArray[17]),
// libcategetab: defaultValue(dataArray[18]),
// categagretab: defaultValue(dataArray[19]),
// libcategagretab: defaultValue(dataArray[20]),
// siret: defaultValue(dataArray[21]),
// codeape: defaultValue(dataArray[22]),
// codemft: defaultValue(dataArray[23]),
// libmft: defaultValue(dataArray[24]),
// codesph: defaultValue(dataArray[25]),
// libsph: defaultValue(dataArray[26]),
// dateouv: defaultValue((new Date(dataArray[27] || '1900-01-01')).toISOString().split('T')[0]), // Utilise la date par défaut si la date est vide
// dateautor: defaultValue((new Date(dataArray[28] || '1900-01-01')).toISOString().split('T')[0]),
// datemaj: defaultValue((new Date(dataArray[29] || '1900-01-01')).toISOString().split('T')[0]),
// numuai: defaultValue(dataArray[30]),
// coordxet: dataArray[31] ? parseFloat(dataArray[31]) : 'NULL',
// coordyet: dataArray[32] ? parseFloat(dataArray[32]) : 'NULL',
// };
// if (typedData.nofinesset === '690051545') {
// console.log(dataArray[20])
// }
// const formattedData = Object.values(typedData).map((value) => (isNaN(value) ? `'${value.replaceAll("'", "''")}'` : (value === null || value === '' ? 'NULL' : value))); // Assurez-vous que les chaînes sont entourées de guillemets simples
// fichierSortieStream.write(`${isFirst ? '' : ',\n'} (${Object.values(typedData).join(', ')})`);
// fichierSortieStream.write('INSERT INTO finess (' + columns.join(', ') + ') VALUES ' + `(${Object.values(typedData).join(', ')});\n`);
fichierSortieStream.write(`${dataArray.join(';')}\n`);
}
let isFirst = true
// Lire chaque ligne du fichier de structures
for await (const ligne of lecteurStructures) {
const valeurs = ligne.split(';');
const structureid = valeurs[1];
// Vérifier si des coordonnées existent pour cette structureid
if (coordonnees.hasOwnProperty(structureid)) {
const { system, coordX, coordY } = coordonnees[structureid];
const coordonneesGPS = transformToGPS(system, coordX, coordY);
if (coordonneesGPS) {
// Écrire les valeurs dans le fichier de sortie
ecrireLigneStructure(`${ligne};${coordonneesGPS.latitude};${coordonneesGPS.longitude}`, isFirst);
} else {
// Si aucune coordonnée n'est disponible, écrire la ligne telle quelle dans le fichier de sortie
ecrireLigneStructure(`${ligne};;`, isFirst);
}
} else {
// Si aucune coordonnée n'est disponible, écrire la ligne telle quelle dans le fichier de sortie
ecrireLigneStructure(`${ligne};;`, isFirst);
}
isFirst = false
}
// Fermer la parenthèse finale et le point-virgule
// fichierSortieStream.write(';');
fichierSortieStream.end();
console.log(`Le fichier SQL a été généré : ${fichierSortie}`);
console.log('Fusion des coordonnées GPS dans le fichier de structures terminée.');
} catch (erreur) {
console.error(`Erreur lors de la fusion des coordonnées GPS : ${erreur.message}`);
}
}

View File

@ -0,0 +1,95 @@
import path from 'path';
import fs from 'fs'
import { writeFile, mkdir } from 'fs/promises'
import readline from 'readline';
export function customSplit(line, separator) {
const parts = [];
let currentPart = '';
let insideQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
insideQuotes = !insideQuotes;
} else if (char === separator && !insideQuotes) {
parts.push(currentPart.trim());
currentPart = '';
} else {
currentPart += char;
}
}
parts.push(currentPart.trim());
return parts;
}
export const loadEnvFile = () => {
// Charger les variables d'environnement à partir du fichier .env
const envPath = './.env'; // Spécifiez le chemin correct si différent
const envFile = fs.readFileSync(envPath, 'utf-8');
const envVariables = envFile.split('\n').reduce((acc, line) => {
const [key, value] = line.split('=');
if (key && value) {
acc[key.trim()] = value.trim();
}
return acc;
}, {});
return envVariables
}
export const downloadAndSaveFile = async (url, filename) => {
const folder = path.dirname(filename)
if (!fs.existsSync(folder)) await mkdir(folder)
const response = await fetch(url)
const buffer = Buffer.from(await response.arrayBuffer())
await writeFile(filename, buffer)
}
export const downloadJSON = async (url) => (await fetch(url)).json()
// Fonction pour lire un fichier ligne par ligne et filtrer les lignes en fonction de la condition
export async function filtrerEtEnregistrerLignes(fichierEntree, fichierSortie1, fichierSortie2, condition) {
try {
// Créer un flux de lecture pour le fichier d'entrée
const lecteur = readline.createInterface({
input: fs.createReadStream(fichierEntree),
crlfDelay: Infinity
});
// Créer un flux d'écriture pour le fichier de sortie 1
const fichierSortieStream1 = fs.createWriteStream(fichierSortie1);
// Créer un flux d'écriture pour le fichier de sortie 2
const fichierSortieStream2 = fs.createWriteStream(fichierSortie2);
// Fonction pour traiter chaque ligne du fichier
const traiterLigne = (ligne) => {
// Appliquer la condition à la ligne
if (condition(ligne)) {
// Écrire la ligne dans le fichier de sortie 1
fichierSortieStream1.write(`${ligne}\n`);
} else {
// Écrire la ligne dans le fichier de sortie 2
fichierSortieStream2.write(`${ligne}\n`);
}
};
// Lire chaque ligne du fichier
for await (const ligne of lecteur) {
traiterLigne(ligne);
}
console.log('Filtrage et enregistrement des lignes terminés.');
// Fermer les flux d'écriture
fichierSortieStream1.end();
fichierSortieStream2.end();
} catch (erreur) {
console.error(`Erreur lors du filtrage et de l'enregistrement des lignes : ${erreur.message}`);
}
}

88
scripts/helpers/gps.js Normal file
View File

@ -0,0 +1,88 @@
import proj4 from 'proj4'
// Définir les paramètres de la projection WGS84 (EPSG:4326) (GPS)
proj4.defs('EPSG:4326', '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs');
// Définir les paramètres de la projection Lambert 93 (EPSG:2154)
proj4.defs('EPSG:2154', '+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs');
// Définir les paramètres de la projection UTM Zone 20N (EPSG:32620)
proj4.defs('EPSG:32620', '+proj=utm +zone=20 +ellps=WGS84 +datum=WGS84 +units=m +no_defs');
// Définir les paramètres de la projection UTM_N21 (exemple)
proj4.defs('EPSG:32621', '+proj=utm +zone=21 +ellps=WGS84 +datum=WGS84 +units=m +no_defs');
// Définir les paramètres de la projection UTM_N22 (exemple)
proj4.defs('EPSG:32622', '+proj=utm +zone=22 +ellps=WGS84 +datum=WGS84 +units=m +no_defs');
// Définir les paramètres de la projection UTM_S38 (exemple)
proj4.defs('EPSG:32638', '+proj=utm +zone=38 +ellps=WGS84 +datum=WGS84 +units=m +no_defs');
// Définir les paramètres de la projection UTM_S40 (exemple)
proj4.defs('EPSG:32740', '+proj=utm +zone=40 +ellps=WGS84 +datum=WGS84 +units=m +no_defs');
export const transformToGPS = (system, coordX, coordY, ) => {
if (system.includes('LAMBERT_93')) {
return lambert93toGPS(coordX, coordY)
}
if (system.includes('UTM_N20')) {
return utmN20toGPS(coordX, coordY)
}
if (system.includes('UTM_N21')) {
return utmN21toGPS(coordX, coordY)
}
if (system.includes('UTM_N22')) {
return utmN22toGPS(coordX, coordY)
}
if (system.includes('UTM_S38')) {
return utmS38toGPS(coordX, coordY)
}
if (system.includes('UTM_S40')) {
return utmS40toGPS(coordX, coordY)
}
console.error(system);
}
function lambert93toGPS(easting, northing) {
// Convertir les coordonnées Lambert 93 en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:2154', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}
function utmN20toGPS(easting, northing) {
// Convertir les coordonnées UTM en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:32620', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}
function utmN21toGPS(easting, northing) {
// Convertir les coordonnées UTM_N21 en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:32621', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}
function utmN22toGPS(easting, northing) {
// Convertir les coordonnées UTM_N22 en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:32622', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}
function utmS38toGPS(easting, northing) {
// Convertir les coordonnées UTM_S38 en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:32638', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}
function utmS40toGPS(easting, northing) {
// Convertir les coordonnées UTM_S40 en WGS84 (GPS)
const [longitude, latitude] = proj4('EPSG:32740', 'EPSG:4326', [easting, northing]);
return { latitude, longitude };
}

7
scripts/transform.js Normal file
View File

@ -0,0 +1,7 @@
import { transformFiness } from './helpers/finess.js'
async function main() {
await transformFiness()
}
main()

10
scripts/transform.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/ash
echo $'- Transformation des données de régions'
cat node_modules/@etalab/decoupage-administratif/data/regions.json | jq -r '.[] | [.code, .chefLieu, .nom, .typeLiaison, .zone] | @csv' > initdb/regions.csv
echo $'- Transformation des données de départements'
cat node_modules/@etalab/decoupage-administratif/data/departements.json | jq -r '.[] | [.code, .region, .chefLieu, .nom, .typeLiaison, .zone] | @csv' > initdb/departements.csv
echo $'- Extraction des données INPI'
unzip -o ./tmp/StockEtablissement_utf8.zip -d ./initdb/