Compare commits

...

4 Commits

Author SHA1 Message Date
Simon C
9b13a144e0 feat: Mise en place du projet 2024-02-08 12:28:51 +01:00
Simon C
10bc230e2e docs: Mise à jour de la documentation 2024-02-06 11:49:02 +01:00
Simon C
b5339522ec feat: Mise à jour du fichier de configuration 2024-02-06 11:48:46 +01:00
Simon C
5a572c7f4a WIP 2024-01-24 16:29:09 +01:00
20 changed files with 988 additions and 2 deletions

35
.env.template Normal file
View File

@ -0,0 +1,35 @@
#
# PostgreSQL
#
POSTGRES_CONTAINER_NAME=postgres
POSTGRES_IMAGE=postgres:latest
POSTGRES_VOLUME_NAME=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
DB_ANON_ROLE=anon
DB_SCHEMA=public
PGADMIN_USER=postgres@postgres.org
PGADMIN_PASSWORD=postgres
#
# PostgREST
#
POSTGREST_URL=https://opendata.example.org/
POSTGREST_CONTAINER_NAME=postgrest
POSTGREST_IMAGE=postgrest/postgrest:v12.0.2
PGRST_DB_MAX_ROWS=100
#
# Swagger UI
#
SWAGGER_URL=https://ui.opendata.example.org/
SWAGGER_CONTAINER_NAME=swagger
SWAGGER_IMAGE=swaggerapi/swagger-ui:v5.11.0

4
.gitignore vendored Normal file
View File

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

View File

@ -1,9 +1,70 @@
# 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. Les données pourront être mise à jour régulièrement pour permettre d'avoir les dernières mise à jour.
## Données
Voici les données dont nous rappatrirons.
### 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 up -d # Création des conteneurs
docker compose 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 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'

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

@ -0,0 +1,29 @@
---
version: "3.8"
networks:
front-end:
back-end:
driver: bridge
services:
postgres:
ports:
- "5432:5432"
networks:
- back-end
postgrest:
ports:
- "3000:3000"
networks:
- back-end
swagger:
ports:
- "8080:8080"
networks:
- front-end
- back-end

View File

@ -0,0 +1,19 @@
---
version: "3.8"
networks:
traefik:
name: ${TRAEFIK_NETWORK_NAME:-traefik}
external: true
opendata:
name: ${OPENDATA_NETWORK_NAME:-opendata}
services:
postgrest:
networks:
- traefik
swagger:
networks:
- traefik

47
docker-compose.yml Normal file
View File

@ -0,0 +1,47 @@
---
version: "3.8"
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
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
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/}

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"
}
}
}
}

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"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/