diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index f28d0d8f..c24c1aa5 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -36,6 +36,7 @@ x-airflow-common: AIRFLOW_VAR_EMPLOIS_API_TOKEN: ${AIRFLOW_VAR_EMPLOIS_API_TOKEN} AIRFLOW_VAR_ENVIRONMENT: ${AIRFLOW_VAR_ENVIRONMENT} AIRFLOW_VAR_FREDO_API_TOKEN: ${AIRFLOW_VAR_FREDO_API_TOKEN} + AIRFLOW_VAR_IMILO_API_SECRET: ${AIRFLOW_VAR_IMILO_API_SECRET} AIRFLOW_VAR_FT_API_TOKEN: ${AIRFLOW_VAR_FT_API_TOKEN} AIRFLOW_VAR_MES_AIDES_AIRTABLE_KEY: ${AIRFLOW_VAR_MES_AIDES_AIRTABLE_KEY} AIRFLOW_VAR_SOLIGUIDE_API_TOKEN: ${AIRFLOW_VAR_SOLIGUIDE_API_TOKEN} diff --git a/deployment/main.tf b/deployment/main.tf index af33be34..de6465ed 100644 --- a/deployment/main.tf +++ b/deployment/main.tf @@ -247,6 +247,7 @@ resource "null_resource" "up" { AIRFLOW_VAR_EMPLOIS_API_TOKEN='${var.emplois_api_token}' AIRFLOW_VAR_ENVIRONMENT='${var.environment}' AIRFLOW_VAR_FREDO_API_TOKEN='${var.fredo_api_token}' + AIRFLOW_VAR_IMILO_API_SECRET='${var.imilo_api_secret}' AIRFLOW_VAR_FT_API_TOKEN='${var.ft_api_token}' AIRFLOW_VAR_MES_AIDES_AIRTABLE_KEY='${var.mes_aides_airtable_key}' AIRFLOW_VAR_SOLIGUIDE_API_TOKEN='${var.soliguide_api_token}' diff --git a/deployment/variables.tf b/deployment/variables.tf index f37cbc4c..96e7903b 100644 --- a/deployment/variables.tf +++ b/deployment/variables.tf @@ -209,3 +209,10 @@ variable "fredo_api_token" { sensitive = true default = "" } + +variable "imilo_api_secret" { + description = "Used in extraction tasks orchestrated by airflow" + type = string + sensitive = true + default = "" +} diff --git a/docker-compose.yml b/docker-compose.yml index 75a253c6..3c5be1c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ x-airflow-common: AIRFLOW_VAR_DORA_API_TOKEN: ${AIRFLOW_VAR_DORA_API_TOKEN} AIRFLOW_VAR_FREDO_API_TOKEN: ${AIRFLOW_VAR_FREDO_API_TOKEN} AIRFLOW_VAR_FT_API_TOKEN: ${AIRFLOW_VAR_FT_API_TOKEN} + AIRFLOW_VAR_IMILO_API_SECRET: ${AIRFLOW_VAR_IMILO_API_SECRET} AIRFLOW_VAR_DORA_PREPROD_API_TOKEN: ${AIRFLOW_VAR_DORA_PREPROD_API_TOKEN} AIRFLOW_VAR_EMPLOIS_API_TOKEN: ${AIRFLOW_VAR_EMPLOIS_API_TOKEN} AIRFLOW_VAR_MES_AIDES_AIRTABLE_KEY: ${AIRFLOW_VAR_MES_AIDES_AIRTABLE_KEY} diff --git a/pipeline/dags/dag_utils/sources/__init__.py b/pipeline/dags/dag_utils/sources/__init__.py index dfd47d75..dfee04b6 100644 --- a/pipeline/dags/dag_utils/sources/__init__.py +++ b/pipeline/dags/dag_utils/sources/__init__.py @@ -13,6 +13,7 @@ emplois_de_linclusion, france_travail, fredo, + imilo, mediation_numerique, mes_aides, reseau_alpha, @@ -221,6 +222,28 @@ }, "odspep": {}, "monenfant": {}, + "imilo": { + "schedule": "@daily", + "snapshot": True, + "extractor": imilo.extract, + "streams": { + "offres": { + "filename": "offres.json", + "url": Variable.get("IMILO_API_URL", None), + "token": Variable.get("IMILO_API_SECRET", None), + }, + "structures": { + "filename": "structures.json", + "url": Variable.get("IMILO_API_URL", None), + "token": Variable.get("IMILO_API_SECRET", None), + }, + "structures_offres": { + "filename": "structures_offres.json", + "url": Variable.get("IMILO_API_URL", None), + "token": Variable.get("IMILO_API_SECRET", None), + }, + }, + }, } diff --git a/pipeline/dags/dag_utils/sources/imilo.py b/pipeline/dags/dag_utils/sources/imilo.py new file mode 100644 index 00000000..4c11832f --- /dev/null +++ b/pipeline/dags/dag_utils/sources/imilo.py @@ -0,0 +1,53 @@ +import json + +import requests + + +class ImiloClient: + def __init__(self, base_url: str, secret: str) -> None: + self.base_url = base_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({"Content-Type": "application/json"}) + self.secret = secret + + # The token lasts 1h + def _get_token(self): + response = self.session.post( + url=f"{self.base_url}/get_token", + data=json.dumps( + { + "client_secret": self.secret, + } + ), + ) + response.raise_for_status() + self.session.headers.update( + {"Authorization": f"Bearer {response.json()['access_token']}"} + ) + + def _get_endpoint( + self, + url_path: str, + ) -> list: + next_url = f"{self.base_url}{url_path}" + response = self.session.get(next_url) + if response.status_code == 401: + self._get_token() + response = self.session.get(next_url) + response.raise_for_status() + return response.json() + + def list_offres(self) -> list: + return self._get_endpoint("/get_offres") + + def list_structures(self) -> list: + return self._get_endpoint("/get_structures") + + def list_structures_offres(self) -> list: + return self._get_endpoint("/get_structures_offres") + + +def extract(id: str, url: str, token: str, **kwargs) -> bytes: + client = ImiloClient(base_url=url, secret=token) + data = getattr(client, f"list_{id}")() + return json.dumps(data).encode() diff --git a/pipeline/dbt/models/_sources.yml b/pipeline/dbt/models/_sources.yml index c763ccac..29c5942e 100644 --- a/pipeline/dbt/models/_sources.yml +++ b/pipeline/dbt/models/_sources.yml @@ -165,3 +165,16 @@ sources: - name: services meta: kind: service + + - name: imilo + schema: imilo + meta: + is_provider: true + tables: + - name: structures + meta: + kind: structure + - name: offres + meta: + kind: service + - name: structures_offres diff --git a/pipeline/dbt/models/intermediate/int__union_adresses.sql b/pipeline/dbt/models/intermediate/int__union_adresses.sql index 67de5374..185f7207 100644 --- a/pipeline/dbt/models/intermediate/int__union_adresses.sql +++ b/pipeline/dbt/models/intermediate/int__union_adresses.sql @@ -10,6 +10,7 @@ WITH adresses AS ( ref('int_finess__adresses'), ref('int_france_travail__adresses'), ref('int_fredo__adresses'), + ref('int_imilo__adresses'), ref('int_mediation_numerique__adresses'), ref('int_mes_aides__adresses'), ref('int_monenfant__adresses'), diff --git a/pipeline/dbt/models/intermediate/int__union_services.sql b/pipeline/dbt/models/intermediate/int__union_services.sql index b24ecc9d..d8fbd2ea 100644 --- a/pipeline/dbt/models/intermediate/int__union_services.sql +++ b/pipeline/dbt/models/intermediate/int__union_services.sql @@ -7,6 +7,7 @@ WITH services AS ( ref('int_dora__services'), ref('int_france_travail__services'), ref('int_fredo__services'), + ref('int_imilo__services'), ref('int_mediation_numerique__services'), ref('int_mes_aides__services'), ref('int_monenfant__services'), diff --git a/pipeline/dbt/models/intermediate/int__union_structures.sql b/pipeline/dbt/models/intermediate/int__union_structures.sql index a0695766..3821cdc9 100644 --- a/pipeline/dbt/models/intermediate/int__union_structures.sql +++ b/pipeline/dbt/models/intermediate/int__union_structures.sql @@ -10,6 +10,7 @@ WITH structures AS ( ref('int_finess__structures'), ref('int_france_travail__structures'), ref('int_fredo__structures'), + ref('int_imilo__structures'), ref('int_mediation_numerique__structures'), ref('int_mes_aides__structures'), ref('int_monenfant__structures'), diff --git a/pipeline/dbt/models/intermediate/quality/_quality_models.yml b/pipeline/dbt/models/intermediate/quality/_quality_models.yml index a56dc1f6..cd7c7387 100644 --- a/pipeline/dbt/models/intermediate/quality/_quality_models.yml +++ b/pipeline/dbt/models/intermediate/quality/_quality_models.yml @@ -23,6 +23,7 @@ models: - emplois_de_linclusion - france_travail - fredo + - imilo - mediation_numerique - mes_aides - monenfant diff --git a/pipeline/dbt/models/intermediate/quality/int_quality__stats.sql b/pipeline/dbt/models/intermediate/quality/int_quality__stats.sql index f5d66029..ed064998 100644 --- a/pipeline/dbt/models/intermediate/quality/int_quality__stats.sql +++ b/pipeline/dbt/models/intermediate/quality/int_quality__stats.sql @@ -20,6 +20,10 @@ -- depends_on: {{ ref('int_france_travail__structures') }} -- depends_on: {{ ref('stg_fredo__structures') }} -- depends_on: {{ ref('int_fredo__structures') }} +-- depends_on: {{ ref('stg_imilo__offres') }} +-- depends_on: {{ ref('stg_imilo__structures') }} +-- depends_on: {{ ref('int_imilo__services') }} +-- depends_on: {{ ref('int_imilo__structures') }} -- depends_on: {{ ref('stg_mediation_numerique__services') }} -- depends_on: {{ ref('stg_mediation_numerique__structures') }} -- depends_on: {{ ref('int_mediation_numerique__services') }} @@ -100,15 +104,15 @@ final AS ( {% for source_node in graph.sources.values() if source_node.source_meta.is_provider %} {% if source_node.meta.kind %} + {% if not loop.first %} + UNION ALL + {% endif %} + {% set source_name = source_node.source_name %} {% set stream_name = source_node.name %} SELECT * FROM {{ source_name }}__{{ stream_name }}__stats - {% if not loop.last %} - UNION ALL - {% endif %} - {% endif %} {% endfor %} diff --git a/pipeline/dbt/models/intermediate/sources/imilo/_imilo__models.yml b/pipeline/dbt/models/intermediate/sources/imilo/_imilo__models.yml new file mode 100644 index 00000000..b3be3292 --- /dev/null +++ b/pipeline/dbt/models/intermediate/sources/imilo/_imilo__models.yml @@ -0,0 +1,49 @@ +version: 2 + +models: + - name: int_imilo__adresses + data_tests: + - check_adresse: + config: + severity: warn + columns: + - name: id + data_tests: + - unique + - not_null + + - name: int_imilo__services + data_tests: + - check_service: + config: + severity: warn + columns: + - name: id + data_tests: + - unique + - not_null + - dbt_utils.not_empty_string + - name: structure_id + data_tests: + - not_null + - relationships: + to: ref('int_imilo__structures') + field: id + + - name: int_imilo__structures + data_tests: + - check_structure: + config: + severity: warn + columns: + - name: id + data_tests: + - unique + - not_null + - dbt_utils.not_empty_string + - name: adresse_id + data_tests: + - not_null + - relationships: + to: ref('int_imilo__adresses') + field: id diff --git a/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__adresses.sql b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__adresses.sql new file mode 100644 index 00000000..9909c500 --- /dev/null +++ b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__adresses.sql @@ -0,0 +1,19 @@ +WITH structures AS ( + SELECT * FROM {{ ref('stg_imilo__structures') }} +), + +final AS ( + SELECT + id AS "id", + commune AS "commune", + code_postal AS "code_postal", + code_insee AS "code_insee", + adresse AS "adresse", + complement_adresse AS "complement_adresse", + CAST(NULL AS FLOAT) AS "longitude", + CAST(NULL AS FLOAT) AS "latitude", + _di_source_id AS "source" + FROM structures +) + +SELECT * FROM final diff --git a/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__services.sql b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__services.sql new file mode 100644 index 00000000..ef6d9e9d --- /dev/null +++ b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__services.sql @@ -0,0 +1,51 @@ +WITH services AS ( + SELECT * FROM {{ ref('stg_imilo__offres') }} +), + +final AS ( + SELECT + services._di_source_id AS "source", + CONCAT( + structures_offres.id_offre, + '_', + structures_offres.id_structure + ) AS "id", + CAST(structures_offres."id_structure" AS TEXT) AS "structure_id", + NULL AS "courriel", + CAST(NULL AS BOOLEAN) AS "cumulable", + CAST(NULL AS BOOLEAN) AS "contact_public", + NULL AS "contact_nom_prenom", + CAST(services.date_maj AS DATE) AS "date_maj", + CAST(services.date_creation AS DATE) AS "date_creation", + NULL AS "formulaire_en_ligne", + NULL AS "frais_autres", + CAST(NULL AS TEXT []) AS "justificatifs", + NULL AS "lien_source", + CAST(NULL AS TEXT []) AS "modes_accueil", + CAST(NULL AS TEXT []) AS "modes_orientation_accompagnateur", + NULL AS "modes_orientation_accompagnateur_autres", + ARRAY[services.modes_orientation_beneficiaire] AS "modes_orientation_beneficiaire", + NULL AS "modes_orientation_beneficiaire_autres", + services.nom AS "nom", + NULL AS "page_web", + NULL AS "presentation_detail", + services.presentation_resume AS "presentation_resume", + NULL AS "prise_rdv", + ARRAY[services.profils] AS "profils", + NULL AS "profils_precisions", + CAST(NULL AS TEXT []) AS "pre_requis", + NULL AS "recurrence", + ARRAY[services.thematiques] AS "thematiques", + CAST(NULL AS TEXT []) AS "types", + NULL AS "telephone", + CAST(NULL AS TEXT []) AS "frais", + NULL AS "zone_diffusion_type", + NULL AS "zone_diffusion_code", + NULL AS "zone_diffusion_nom", + CAST(NULL AS DATE) AS "date_suspension" + FROM services + LEFT JOIN {{ ref('stg_imilo__structures_offres') }} AS structures_offres + ON services.id = structures_offres.id_offre +) + +SELECT * FROM final diff --git a/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__structures.sql b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__structures.sql new file mode 100644 index 00000000..e49319f9 --- /dev/null +++ b/pipeline/dbt/models/intermediate/sources/imilo/int_imilo__structures.sql @@ -0,0 +1,31 @@ +WITH structures AS ( + SELECT * FROM {{ ref('stg_imilo__structures') }} +), + +final AS ( + SELECT + _di_source_id AS "source", + id AS "id", + siret AS "siret", + NULL AS "rna", + courriel AS "courriel", + CAST(NULL AS BOOLEAN) AS "antenne", + horaires_ouverture AS "horaires_ouverture", + site_web AS "site_web", + NULL AS "lien_source", + NULL AS "accessibilite", + telephone AS "telephone", + typologie AS "typologie", + nom AS "nom", + ARRAY[labels_nationaux] AS "labels_nationaux", + CAST(NULL AS TEXT []) AS "labels_autres", + presentation_resume AS "presentation_resume", + presentation_detail AS "presentation_detail", + id AS "adresse_id", + CAST(NULL AS TEXT []) AS "thematiques", + CAST(date_maj AS DATE) AS "date_maj" + FROM structures + +) + +SELECT * FROM final diff --git a/pipeline/dbt/models/staging/sources/imilo/_imilo__models.yml b/pipeline/dbt/models/staging/sources/imilo/_imilo__models.yml new file mode 100644 index 00000000..0f8e0a41 --- /dev/null +++ b/pipeline/dbt/models/staging/sources/imilo/_imilo__models.yml @@ -0,0 +1,123 @@ +version: 2 + +models: + - name: stg_imilo__offres + columns: + - name: id + data_tests: + - unique + - not_null + - dbt_utils.not_empty_string + - name: nom + data_tests: + - not_null + - dbt_utils.not_empty_string + - name: date_maj + data_tests: + - not_null + - name: date_creation + data_tests: + - not_null: + config: + severity: warn + - dbt_utils.not_constant + - name: thematiques + data_tests: + - not_null + - dbt_utils.not_empty_string + - relationships: + to: ref('thematiques') + field: value + - name: presentation_resume + data_tests: + - dbt_utils.not_empty_string + - name: modes_accueil + data_tests: + - not_null + - dbt_utils.not_empty_string + - relationships: + to: ref('modes_accueil') + field: value + - name: profils + data_tests: + - dbt_utils.expression_is_true: + expression: "<@ ARRAY(SELECT value FROM {{ ref('profils') }})" + - name: modes_orientation_beneficiaire + data_tests: + - not_null + - dbt_utils.not_empty_string + - relationships: + to: ref('modes_orientation_beneficiaire') + field: value + + - name: stg_imilo__structures_offres + columns: + - name: id_offre + data_tests: + - not_null + - name: id_structure + data_tests: + - not_null + + - name: stg_imilo__structures + columns: + - name: id + data_tests: + - unique + - not_null + - dbt_utils.not_empty_string + - name: courriel + data_tests: + - not_null + - dbt_utils.not_empty_string + - name: antenne + - name: siret + data_tests: + - dbt_utils.not_empty_string + - name: commune + data_tests: + - not_null + - dbt_utils.not_empty_string + - name: horaires_ouverture + data_tests: + - dbt_utils.not_empty_string + - name: site_web + data_tests: + - dbt_utils.not_empty_string + - name: telephone + data_tests: + - dbt_utils.not_empty_string + - name: typologie + data_tests: + - dbt_utils.not_empty_string + - name: code_insee + data_tests: + - dbt_utils.not_empty_string + - name: code_postal + data_tests: + - dbt_utils.not_empty_string + - name: nom + data_tests: + - not_null: + config: + severity: warn + - dbt_utils.not_constant + - dbt_utils.not_empty_string + - name: labels_nationaux + data_tests: + - dbt_utils.not_empty_string + - name: adresse + data_tests: + - dbt_utils.not_empty_string + - name: presentation_resume + data_tests: + - dbt_utils.not_empty_string + - name: presentation_detail + data_tests: + - dbt_utils.not_empty_string + - name: complement_adresse + data_tests: + - dbt_utils.not_empty_string + - name: date_maj + data_tests: + - not_null diff --git a/pipeline/dbt/models/staging/sources/imilo/stg_imilo__offres.sql b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__offres.sql new file mode 100644 index 00000000..276df85b --- /dev/null +++ b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__offres.sql @@ -0,0 +1,31 @@ +WITH source AS ( + {{ stg_source_header('imilo', 'offres') }} +), + +final AS ( + SELECT + source._di_source_id AS "_di_source_id", + NULLIF(TRIM(source.data -> 'offres' ->> 'id_offre'), '') AS "id", + CAST((source.data -> 'offres' ->> 'date_maj') AS TIMESTAMP WITH TIME ZONE) AS "date_maj", + NULLIF(TRIM(source.data -> 'offres' ->> 'nom_dora'), '') AS "nom", + NULLIF(TRIM(source.data -> 'offres' ->> 'thematique'), '') AS "thematiques", + CAST((source.data -> 'offres' ->> 'date_import') AS TIMESTAMP WITH TIME ZONE) AS "date_creation", + NULLIF(TRIM(source.data -> 'offres' ->> 'description'), '') AS "presentation_resume", + NULLIF(TRIM(source.data -> 'offres' ->> 'modes_accueil'), '') AS "modes_accueil", + CASE + WHEN source.data -> 'offres' ->> 'liste_des_profils' IS NULL THEN NULL + ELSE ARRAY( + SELECT + CASE + WHEN profils = 'Jeunes de 16 à 25 ans' THEN 'jeunes-16-26' + WHEN profils = 'RQTH moins de 30 ans' THEN 'personnes-handicapees' + ELSE profils -- original value if it doesn't match any pattern, then it should fail the test + END + FROM UNNEST(STRING_TO_ARRAY(source.data -> 'offres' ->> 'liste_des_profils', ';')) AS profils + ) + END AS "profils", + NULLIF(TRIM(source.data -> 'offres' ->> 'modes_orientation_beneficiaire'), '') AS "modes_orientation_beneficiaire" + FROM source +) + +SELECT * FROM final diff --git a/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures.sql b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures.sql new file mode 100644 index 00000000..e9c2c722 --- /dev/null +++ b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures.sql @@ -0,0 +1,28 @@ +WITH source AS ( + {{ stg_source_header('imilo', 'structures') }} +), + +final AS ( + SELECT + _di_source_id AS "_di_source_id", + CAST(data -> 'structures' ->> 'id_structure' AS TEXT) AS "id", + NULLIF(TRIM(data -> 'structures' ->> 'email'), '') AS "courriel", + NULLIF(TRIM(data -> 'structures' ->> 'commune'), '') AS "commune", + NULLIF(TRIM(data -> 'structures' ->> 'siret'), '') AS "siret", + NULLIF(TRIM(data -> 'structures' ->> 'horaires'), '') AS "horaires_ouverture", + NULLIF(TRIM(data -> 'structures' ->> 'site_web'), '') AS "site_web", + NULLIF(TRIM(data -> 'structures' ->> 'telephone'), '') AS "telephone", + NULLIF(TRIM(data -> 'structures' ->> 'typologie'), '') AS "typologie", + NULLIF(TRIM(data -> 'structures' ->> 'code_insee'), '') AS "code_insee", + NULLIF(TRIM(data -> 'structures' ->> 'code_postal'), '') AS "code_postal", + NULLIF(TRIM(data -> 'structures' ->> 'nom_structure'), '') AS "nom", + NULLIF(TRIM(data -> 'structures' ->> 'labels_nationaux'), '') AS "labels_nationaux", + NULLIF(TRIM(data -> 'structures' ->> 'adresse_structure'), '') AS "adresse", + NULLIF(TRIM(data -> 'structures' ->> 'presentation_resumee'), '') AS "presentation_resume", + NULLIF(TRIM(data -> 'structures' ->> 'presentation_detaillee'), '') AS "presentation_detail", + NULLIF(TRIM(data -> 'structures' ->> 'complement_adresse_structure'), '') AS "complement_adresse", + CAST((data -> 'structures' ->> 'date_maj') AS TIMESTAMP WITH TIME ZONE) AS "date_maj" + FROM source +) + +SELECT * FROM final diff --git a/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures_offres.sql b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures_offres.sql new file mode 100644 index 00000000..5afcd3e3 --- /dev/null +++ b/pipeline/dbt/models/staging/sources/imilo/stg_imilo__structures_offres.sql @@ -0,0 +1,5 @@ +SELECT + CAST((data -> 'structures_offres' ->> 'id') AS TEXT) AS "id", + CAST((data -> 'structures_offres' ->> 'offre_id') AS TEXT) AS "id_offre", + CAST((data -> 'structures_offres' ->> 'missionlocale_id') AS TEXT) AS "id_structure" +FROM {{ source('imilo', 'structures_offres') }} diff --git a/pipeline/defaults.env b/pipeline/defaults.env index bdb3749e..c2f4f0c9 100644 --- a/pipeline/defaults.env +++ b/pipeline/defaults.env @@ -15,6 +15,7 @@ AIRFLOW_VAR_EMPLOIS_API_URL=https://emplois.inclusion.beta.gouv.fr/api/v1/struct AIRFLOW_VAR_ETAB_PUB_FILE_URL=https://www.data.gouv.fr/fr/datasets/r/73302880-e4df-4d4c-8676-1a61bb997f3d AIRFLOW_VAR_FINESS_FILE_URL=https://www.data.gouv.fr/fr/datasets/r/3dc9b1d5-0157-440d-a7b5-c894fcfdfd45 AIRFLOW_VAR_INSEE_FIRSTNAME_FILE_URL=https://www.insee.fr/fr/statistiques/fichier/2540004/nat2021_csv.zip +AIRFLOW_VAR_IMILO_API_URL=https://api-ods.dsiml.org AIRFLOW_VAR_MEDNUM_API_URL=https://cartographie.societenumerique.gouv.fr/api/v0/ AIRFLOW_VAR_MES_AIDES_GARAGES_URL=https://airtable.com/appRga7C9USklxYiV/tblfhYoBpcQoJwGIv/viwoJsw0vsAnU0fAo AIRFLOW_VAR_MES_AIDES_PERMIS_VELO_URL=https://airtable.com/appRga7C9USklxYiV/tblcAC5yMV3Ftzv5c/viwMte3unsIYXxY9a