#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Generate the deterministic demonstration dataset for the SRM-RSK dashboard. The official limit values come from the attached "Cahier des charges d'assainissement liquide de la SRM-RSK" (Articles 42, 44 and 45). Industrial codes are numbered per provincial / prefectural direction: IN + CODE_DP + "-" + NNNN IN = Industrie | DP = Direction Provinciale / Préfectorale DPK = Direction Provinciale de Kénitra -> INDPK-0001, INDPK-0002, ... The industrial records and measurements below are illustrative demo data, except the four sample rows reproduced from the project presentation. """ import json import random from datetime import date, timedelta from pathlib import Path OUT = Path(__file__).with_name("seed_data.json") rng = random.Random(20261103) # --------------------------------------------------------------------------- # 0. Provincial / prefectural directions and new industrial code format # --------------------------------------------------------------------------- DP_CODES = { "Rabat": "DPR", "Salé": "DPS", "Skhirat-Témara": "DPST", "Kénitra": "DPK", "Khemisset": "DPKH", "Sidi Kacem": "DPSK", "Sidi Slimane": "DPSS", } DIRECTIONS = [ {"id": "DPR", "code_dp": "DPR", "libelle": "Direction provinciale / préfectorale de Rabat", "province": "Rabat", "ordre": 1}, {"id": "DPS", "code_dp": "DPS", "libelle": "Direction provinciale / préfectorale de Salé", "province": "Salé", "ordre": 2}, {"id": "DPST", "code_dp": "DPST", "libelle": "Direction provinciale de Skhirat-Témara", "province": "Skhirat-Témara", "ordre": 3}, {"id": "DPK", "code_dp": "DPK", "libelle": "Direction provinciale de Kénitra", "province": "Kénitra", "ordre": 4}, {"id": "DPKH", "code_dp": "DPKH", "libelle": "Direction provinciale de Khemisset", "province": "Khemisset", "ordre": 5}, {"id": "DPSK", "code_dp": "DPSK", "libelle": "Direction provinciale de Sidi Kacem", "province": "Sidi Kacem", "ordre": 6}, {"id": "DPSS", "code_dp": "DPSS", "libelle": "Direction provinciale de Sidi Slimane", "province": "Sidi Slimane", "ordre": 7}, ] def industry_code(province: str, order: int) -> str: return f"IN{DP_CODES[province]}-{order:04d}" # --------------------------------------------------------------------------- # 1. Regulatory parameters (versioned reference data in the target Oracle DB) # --------------------------------------------------------------------------- P = [] def param(code, libelle, unite, vmin, vmax, famille, article, note="", actif=True, derived=False, order=None, source="Cahier des charges SRM-RSK"): P.append({ "code": code, "libelle": libelle, "unite": unite, "min": vmin, "max": vmax, "famille": famille, "article": article, "note": note, "actif": actif, "derived": derived, "ordre": order if order is not None else len(P) + 1, "source": source, }) # Article 42 - conditions generales d'admissibilite param("PH", "pH", "-", 5.5, 8.5, "Admissibilité générale", "Art. 42", "Entre 5,5 et 8,5. Exception: 5,5 à 9,5 si neutralisation à la chaux (dérogation à valider).") param("TEMPERATURE", "Température", "°C", None, 30, "Admissibilité générale", "Art. 42", "Ramenée à une température inférieure ou au plus égale à 30 °C.") param("MES", "Matières en suspension (MES)", "mg/L", None, 500, "Admissibilité générale", "Art. 42", "Ne pas contenir plus de 500 mg/L de MES.") param("DBO5", "Demande biochimique en oxygène (DBO5)", "mg/L", None, 500, "Admissibilité générale", "Art. 42", "DBO5 inférieure ou égale à 500 mg/L.") param("DCO", "Demande chimique en oxygène (DCO)", "mg/L", None, 1000, "Admissibilité générale", "Art. 42", "DCO inférieure à 1000 mg/L.") param("AZOTE_TOTAL", "Azote total (exprimé en N)", "mg/L", None, 150, "Admissibilité générale", "Art. 42", "150 mg/L si exprimé en azote élémentaire.") param("AMMONIUM", "Azote ammoniacal (ion ammonium)", "mg/L", None, 200, "Admissibilité générale", "Art. 42", "200 mg/L si exprimé en ion ammonium. Alternatif à AZOTE_TOTAL.") param("CONDUCTIVITE", "Conductivité", "µS/cm", None, 2700, "Admissibilité générale", "Art. 42", "Conductivité 2700 µS/cm.") param("HUILES_GRAISES", "Huiles et graisses", "mg/L", None, 150, "Admissibilité générale", "Art. 42", "Huiles et graisses 150 mg/L.") param("ECOTOX", "Écotoxicité conforme AFNOR T90.301", "0/1", 1, 1, "Contrôle qualitatif", "Art. 42", "1 = conforme, 0 = non conforme. Saisir 0 ou 1.") param("CYCLIQUES_HALOGENES", "Composés cycliques hydroxylés / dérivés halogénés", "0/1", None, 0, "Contrôle qualitatif", "Art. 42", "0 = absent, 1 = présence interdite.") param("ATTEINTE_TRAITEMENT", "Substances nuisant au traitement biologique / ouvrages", "0/1", None, 0, "Contrôle qualitatif", "Art. 42", "0 = absent, 1 = présence interdite.") param("DEVERSEMENT_INTERDIT", "Corps ou matières interdits (Art. 45)", "0/1", None, 0, "Contrôle qualitatif", "Art. 45", "0 = aucun déversement interdit, 1 = au moins un déversement interdit.") param("PH_CHAUX", "pH - dérogation neutralisation à la chaux", "-", 5.5, 9.5, "Dérogation", "Art. 42", "Utilisable uniquement avec dérogation validée et tracée.", actif=False) # Article 44 - substances nocives art44 = [ ("FE", "Fer (Fe)", 0.5), ("AL", "Aluminium (Al)", 5), ("MGOH2", "Magnésium hydroxyde (Mg(OH)2)", 100), ("CD", "Cadmium (Cd)", 0.5), ("SO4", "Sulfate ((SO4)2-)", 400), ("CR6", "Chrome hexavalent (Cr6+)", 0.1), ("CU", "Cuivre (Cu)", 1), ("CO", "Cobalt (Co)", 2), ("ZN", "Zinc (Zn)", 5), ("HG", "Mercure (Hg)", 0.05), ("NI", "Nickel (Ni)", 2), ("AG", "Argent (Ag)", 0.1), ("PB", "Plomb (Pb)", 0.1), ("CL2", "Chlore libre (Cl2)", 3), ("AS", "Arsenic (As)", 1), ("S2", "Sulfures (S2-)", 1), ("CRO3", "Chromates (CrO3)", 2), ("F", "Fluorure (F-)", 10), ("CN", "Cyanure (CN-)", 0.5), ("NO2", "Nitrites (NO2)", 10), ("PHENOL", "Phénol (C6H5OH)", 5), ("SN", "Étain (Sn)", 0.1), ("P", "Phosphore (P)", 20), ("MN", "Manganèse (Mn)", 2), ("CL", "Chlorures (Cl-)", 1500), ] for code, lib, lim in art44: famille = "Métaux" if code in {"FE", "AL", "CD", "CR6", "CU", "CO", "ZN", "HG", "NI", "AG", "PB", "SN", "MN"} else "Composés chimiques" param(code, lib, "mg/L", None, lim, famille, "Art. 44", f"Valeur limite: {str(lim).replace('.', ',')} mg/L. Liste non limitative pouvant être complétée.") param("TOTAL_METAUX", "Total métaux lourds (Cd, Cu, Zn, Cr, Ni, Fe)", "mg/L", None, 15, "Indicateur dérivé", "Art. 44", "Calculé automatiquement à partir de CD, CU, ZN, CR6, NI et FE saisis sur le même prélèvement. Non saisissable.", derived=True) # --------------------------------------------------------------------------- # 2. Zones and industrial sites # --------------------------------------------------------------------------- ZONES = [ {"id": "Z01", "code": "RTE110", "libelle": "RTE 110", "province": "Kénitra", "lat": 34.24, "lon": -6.62}, {"id": "Z02", "code": "RP1", "libelle": "RP1", "province": "Kénitra", "lat": 34.27, "lon": -6.59}, {"id": "Z03", "code": "ZISAPINO", "libelle": "ZI SAPINO", "province": "Sidi Kacem", "lat": 34.21, "lon": -5.72}, {"id": "Z04", "code": "ZIKENITRA", "libelle": "ZI Kénitra", "province": "Kénitra", "lat": 34.25, "lon": -6.55}, {"id": "Z05", "code": "ZITEMARA", "libelle": "ZI Témara", "province": "Skhirat-Témara", "lat": 33.92, "lon": -7.00}, {"id": "Z06", "code": "ZIAA", "libelle": "ZI Aïn Aouda", "province": "Rabat", "lat": 33.98, "lon": -6.78}, {"id": "Z07", "code": "ZISALE", "libelle": "ZI Salé", "province": "Salé", "lat": 34.06, "lon": -6.76}, {"id": "Z08", "code": "ZFK", "libelle": "Zone Franche Kénitra", "province": "Kénitra", "lat": 34.29, "lon": -6.61}, {"id": "Z09", "code": "ZIKH", "libelle": "ZI Khemisset", "province": "Khemisset", "lat": 33.82, "lon": -6.07}, {"id": "Z10", "code": "ZISLIMANE", "libelle": "ZI Sidi Slimane", "province": "Sidi Slimane", "lat": 34.26, "lon": -5.93}, ] INDS = [ ("NEW WASH", "Textile", "Z01", "Kénitra", "Actif", 34.244, -6.625, 120, 14000, "2×8 h"), ("PROCTER & GAMBLE", "Chimie", "Z02", "Kénitra", "Actif", 34.271, -6.590, 430, 52000, "3×8 h"), ("LESIEUR", "Agroalimentaire", "Z09", "Khemisset", "Actif", 33.818, -6.066, 310, 38000, "2×8 h"), ("LA MAISON DU BOULANGER", "Boulangerie", "Z03", "Sidi Kacem", "Arrêt", 34.212, -5.716, 65, 5200, "1×8 h"), ("TANNERIE X", "Tannerie", "Z04", "Kénitra", "Actif", 34.252, -6.552, 95, 11000, "2×8 h"), ("TEXTILE Y", "Textile", "Z07", "Salé", "Actif", 34.061, -6.762, 180, 21000, "2×8 h"), ("ATLANTIC PACK", "Papier / Carton", "Z05", "Skhirat-Témara", "Actif", 33.923, -7.001, 140, 19000, "2×8 h"), ("SOMACOS", "Métallurgie", "Z08", "Kénitra", "Actif", 34.289, -6.609, 220, 27000, "3×8 h"), ("RAJA FOOD", "Agroalimentaire", "Z06", "Rabat", "Actif", 33.982, -6.781, 155, 16500, "2×8 h"), ("SEBOU CHIMIE", "Chimie", "Z10", "Sidi Slimane", "Actif", 34.261, -5.928, 88, 12500, "2×8 h"), ("MAGHREB TEXTILE", "Textile", "Z01", "Kénitra", "Actif", 34.239, -6.631, 260, 30000, "3×8 h"), ("OUED SEBOU INDUSTRIES", "Agroalimentaire", "Z02", "Kénitra", "À vérifier", 34.266, -6.601, 72, 8800, "1×8 h"), ] INDUSTRIELS = [] NEW_IDS = [] seq_by_dp = {} for i, (nom, secteur, zid, prov, statut, lat, lon, eff, surf, regime) in enumerate(INDS, 1): code_dp = DP_CODES[prov] order = seq_by_dp.get(code_dp, 0) + 1 seq_by_dp[code_dp] = order new_id = industry_code(prov, order) NEW_IDS.append(new_id) INDUSTRIELS.append({ "id": new_id, "code": new_id, "ancien_code": f"IND-{i:04d}", "direction_id": code_dp, "code_dp": code_dp, "ordre_dp": order, "raison_sociale": nom, "secteur": secteur, "zone_id": zid, "province": prov, "statut": statut, "ice": f"{100000000000000 + i * 137:015d}", "rc": f"RC-{20000 + i}", "gps_lat": lat, "gps_lon": lon, "contact": f"Contact exploitation {i}", "telephone": f"05{30000000 + i:08d}"[:10], "email": f"contact{i}@industriel.ma", "effectif": eff, "surface_m2": surf, "capacite": f"{eff * 12} unités/jour", "regime_travail": regime, "date_implantation": f"{2005 + i}-06-15", "cree_le": "2026-01-05T08:30:00", }) # --------------------------------------------------------------------------- # 3. Samples and raw results (the ONLY data the user has to enter) # --------------------------------------------------------------------------- PRELEVEMENTS = [] RESULTATS = [] p_count = 0 r_count = 0 def add_sample(ind_id, d, results, point="Rejet principal", laboratoire="Laboratoire SRM-RSK", reference=None, commentaire=""): global p_count, r_count p_count += 1 pid = f"P-{p_count:05d}" PRELEVEMENTS.append({ "id": pid, "industriel_id": ind_id, "date": d, "point": point, "laboratoire": laboratoire, "type": "REJET_INDUSTRIEL", "reference": reference or f"ECH-2026-{p_count:04d}", "commentaire": commentaire, "cree_le": d + "T09:00:00", }) for code, valeur in results.items(): r_count += 1 RESULTATS.append({ "id": f"R-{r_count:06d}", "prelevement_id": pid, "code_parametre": code, "valeur": float(valeur), "saisi_par": "Laboratoire SRM-RSK", "date_saisie": d + "T10:15:00", }) return pid # Four rows reproduced exactly from the project presentation, page 6. add_sample(NEW_IDS[0], "2026-01-29", {"PH": 6.8, "MES": 320, "DCO": 850, "DBO5": 280}, reference="ECH-PRES-001", commentaire="Ligne reproduite de la présentation synthétique (conforme).") add_sample(NEW_IDS[1], "2026-02-15", {"PH": 8.5, "MES": 180, "DCO": 420, "DBO5": 150}, reference="ECH-PRES-002", commentaire="Ligne reproduite de la présentation synthétique (conforme).") add_sample(NEW_IDS[4], "2026-03-03", {"PH": 4.2, "MES": 1200, "DCO": 2500, "DBO5": 980}, reference="ECH-PRES-003", commentaire="Ligne reproduite de la présentation synthétique (non conforme).") add_sample(NEW_IDS[5], "2026-03-18", {"PH": 9.8, "MES": 680, "DCO": 1800, "DBO5": 620}, reference="ECH-PRES-004", commentaire="Ligne reproduite de la présentation synthétique (non conforme).") # Deterministic illustrative series, Jan-Oct 2026. ~22% of samples have a real exceedance. for mois in range(1, 11): d = f"2026-{mois:02d}-12" for idx, ind in enumerate(INDUSTRIELS): if ind["statut"] == "Arrêt" or rng.random() > 0.72: continue exceed = rng.random() < 0.22 if exceed: mode = rng.choice(["ph", "mes", "dco", "dbo5", "metal"]) ph = rng.choice([4.6, 4.9, 9.2, 9.6]) mes = rng.randint(600, 1500) dco = rng.randint(1100, 2800) dbo5 = rng.randint(520, 980) else: mode = None ph = round(rng.uniform(6.2, 8.2), 1) mes = rng.randint(90, 460) dco = rng.randint(220, 930) dbo5 = rng.randint(80, 460) vals = {"PH": ph, "MES": mes, "DCO": dco, "DBO5": dbo5} if mode == "ph": vals["PH"] = rng.choice([4.6, 4.9, 9.2, 9.6]) if mode == "mes": vals["MES"] = rng.randint(600, 1500) if mode == "dco": vals["DCO"] = rng.randint(1100, 2800) if mode == "dbo5": vals["DBO5"] = rng.randint(520, 980) if mode is None and rng.random() < 0.55: vals["TEMPERATURE"] = round(rng.uniform(19, 28.5), 1) if rng.random() < 0.30: vals["HUILES_GRAISES"] = round(rng.uniform(20, 135), 1) # Some metal panels: the "Total métaux lourds" is computed by the app, never typed. if rng.random() < 0.28: vals["CD"] = round(rng.uniform(0.02, 0.42), 3) vals["CU"] = round(rng.uniform(0.1, 0.85), 3) vals["ZN"] = round(rng.uniform(0.4, 4.2), 2) vals["CR6"] = round(rng.uniform(0.01, 0.08), 3) vals["NI"] = round(rng.uniform(0.2, 1.7), 2) vals["FE"] = round(rng.uniform(0.05, 0.42), 3) if mode == "metal": vals["CR6"] = round(rng.uniform(0.12, 0.35), 3) vals["CU"] = round(rng.uniform(1.2, 2.4), 2) add_sample(ind["id"], d, vals, point="Rejet principal" if idx % 3 else "Sortie station de prétraitement", commentaire="Jeu d'essai déterministe généré pour la démonstration.") # --------------------------------------------------------------------------- # 4. Ancillary operational data (still entered once, never used to type a KPI) # --------------------------------------------------------------------------- ENQUETES = [] for i, ind in enumerate(INDUSTRIELS[:-1], 1): # 11 / 12 -> 91.7% coverage ENQUETES.append({ "id": f"E-{i:04d}", "industriel_id": ind["id"], "date": f"2026-{(i % 10) + 1:02d}-05", "type": "Enquête terrain" if i % 2 else "Contrôle inopiné", "agent": f"Enquêteur {i:02d}", "statut": "Réalisée", "observations": "Raccordements, prétraitement et registre de déversement vérifiés.", }) RECLAMATIONS = [] durations = [4, 7, 5, 9, 3, 8, 6, 4, 11, 5] # mean = 6.2 days for i, dur in enumerate(durations, 1): start = date(2026, (i % 10) + 1, 3) RECLAMATIONS.append({ "id": f"RC-{i:04d}", "industriel_id": INDUSTRIELS[(i * 3) % len(INDUSTRIELS)]["id"], "type": "Réclamation" if i % 3 else "Incident environnemental", "date_ouverture": start.isoformat(), "date_cloture": (start + timedelta(days=dur)).isoformat(), "statut": "Clôturée", "objet": "Odeurs / rejet / nuisance signalée", "gravite": ["Faible", "Moyenne", "Élevée"][i % 3], }) for i in (11, 12): RECLAMATIONS.append({ "id": f"RC-{i:04d}", "industriel_id": INDUSTRIELS[i - 9]["id"], "type": "Incident environnemental", "date_ouverture": f"2026-10-{i:02d}", "date_cloture": None, "statut": "Ouverte", "objet": "Dépassement en cours d'instruction", "gravite": "Élevée", }) # 2025 consumption scaled to the 4.2 M m3 reference figure (demo only). weights = [rng.uniform(0.6, 1.8) for _ in range(12 * 12)] scale = 4_200_000 / sum(weights) CONSOMMATIONS = [] k = 0 for mois in range(1, 13): for ind in INDUSTRIELS: k += 1 CONSOMMATIONS.append({ "id": f"C-{k:04d}", "industriel_id": ind["id"], "periode": f"2025-{mois:02d}", "volume_m3": round(weights[k - 1] * scale, 1), "source": "Relevé compteur", "cree_le": f"2025-{mois:02d}-28T12:00:00", }) # A few 2026 records so period filtering is demonstrable. for mois in range(1, 11): for ind in INDUSTRIELS[:6]: k += 1 CONSOMMATIONS.append({ "id": f"C-{k:04d}", "industriel_id": ind["id"], "periode": f"2026-{mois:02d}", "volume_m3": round(rng.uniform(8000, 42000), 1), "source": "Relevé compteur", "cree_le": f"2026-{mois:02d}-28T12:00:00", }) DOSSIERS = [] for i in range(1, 15): # 14 active MED, matching the presentation's reference value ind = INDUSTRIELS[(i * 5) % len(INDUSTRIELS)] DOSSIERS.append({ "id": f"D-{i:04d}", "industriel_id": ind["id"], "type": "MED", "numero": f"MED-2026-{i:03d}", "date_debut": f"2026-{(i % 9) + 1:02d}-01", "date_echeance": f"2027-{(i % 12) + 1:02d}-28", "statut": "Active", "autorite": "Cellule Environnement SRM-RSK", "objet": "Mise en demeure - mise en conformité des rejets", "cree_le": "2026-01-10T08:00:00", }) DOSSIERS += [ {"id": "D-0015", "industriel_id": NEW_IDS[1], "type": "EIE", "numero": "EIE-2024-018", "date_debut": "2024-03-01", "date_echeance": "2029-03-01", "statut": "Validee", "autorite": "Cellule Environnement", "objet": "Étude d'impact environnemental", "cree_le": "2024-03-01T08:00:00"}, {"id": "D-0016", "industriel_id": NEW_IDS[0], "type": "AUTORISATION", "numero": "AUT-2023-104", "date_debut": "2023-01-15", "date_echeance": "2028-01-15", "statut": "Validee", "autorite": "SRM-RSK", "objet": "Convention spéciale de déversement", "cree_le": "2023-01-15T08:00:00"}, {"id": "D-0017", "industriel_id": NEW_IDS[2], "type": "CERTIFICATION", "numero": "ISO-14001-2025", "date_debut": "2025-05-01", "date_echeance": "2028-05-01", "statut": "Validee", "autorite": "Organisme certificateur", "objet": "ISO 14001", "cree_le": "2025-05-01T08:00:00"}, ] DOCUMENTS = [ {"id": "DOC-0001", "industriel_id": NEW_IDS[0], "contexte": "PRELEVEMENT", "source_id": "P-00001", "type": "Rapport d'analyse", "nom": "Rapport_ANALYSE_INDPK-0001_2026-01-29.pdf", "url": "sharepoint/analyses/2026/01/", "date": "2026-01-29"}, {"id": "DOC-0002", "industriel_id": NEW_IDS[4], "contexte": "DOSSIER", "source_id": "D-0005", "type": "Mise en demeure", "nom": "MED-2026-005.pdf", "url": "sharepoint/conformite/med/", "date": "2026-06-01"}, {"id": "DOC-0003", "industriel_id": NEW_IDS[1], "contexte": "INDUSTRIEL", "source_id": NEW_IDS[1], "type": "Plan de raccordement", "nom": "Plan_branchements_PG.dwg", "url": "sharepoint/sig/plans/", "date": "2025-11-12"}, {"id": "DOC-0004", "industriel_id": NEW_IDS[5], "contexte": "RECLAMATION", "source_id": "RC-0006", "type": "Photo", "nom": "Photo_rejet_2026-07-03.jpg", "url": "sharepoint/incidents/photos/", "date": "2026-07-03"}, ] JOURNAL = [ {"id": "J-000001", "date": "2026-11-03T09:00:00", "utilisateur": "Administrateur", "action": "IMPORT_REFERENTIEL", "entite": "REF_SEUIL_REGLEMENTAIRE", "detail": "Import des valeurs limites Art. 42/44/45 du cahier des charges SRM-RSK (40 paramètres)."}, {"id": "J-000002", "date": "2026-11-03T09:01:00", "utilisateur": "Administrateur", "action": "IMPORT_CANEVAS", "entite": "NOMENCLATURE", "detail": "Transposition des 11 onglets du canevas de collecte en tables relationnelles."}, {"id": "J-000003", "date": "2026-11-03T09:02:00", "utilisateur": "Laboratoire SRM-RSK", "action": "IMPORT_DEMO", "entite": "PRELEVEMENT", "detail": f"Chargement du jeu de démonstration : {len(PRELEVEMENTS)} prélèvements / {len(RESULTATS)} résultats bruts. Codification IN+CODE_DP+NNNN appliquée."}, ] STATE = { "meta": { "version": 2, "generated_at": "2026-11-03T09:02:00", "organisation": "Société Régionale Multiservices Rabat-Salé-Kénitra (SRM-RSK)", "perimetre": "Région Rabat-Salé-Kénitra — suivi environnemental et industriel", "code_format": "IN + CODE_DP + '-' + NNNN : IN=Industrie, DP=Direction Provinciale/Préfectorale, NNNN=ordre dans la direction", "code_examples": { "INDPK-0001": "1er industriel de la Direction provinciale de Kénitra", "INDPKH-0001": "1er industriel de la Direction provinciale de Khemisset", "INDPS-0001": "1er industriel de la Direction préfectorale de Salé" }, "source_docs": [ "Visualisation_Base_Donnees_Industriels_SRM-RSK.pdf (10 pages, juin 2026)", "Base_Donnees_Industriels_SRM-RSK-03-11-2026.csv (11 onglets)", "valeurs limites du cahier des charges d'assainissement liquide de la SRM-RSK.pdf (Art. 42 à 48)", ], "demo_notice": "Données de démonstration. Les 4 prélèvements de la présentation sont reproduits à l'identique ; les autres mesures sont un jeu d'essai déterministe. Les valeurs limites réglementaires sont celles du document joint. Les codes industriels sont désormais numérotés par direction provinciale / préfectorale.", "reference_kpi": { "industriels_recenses": 247, "taux_conformite": 78, "couverture_enquetes": 92, "med_actives": 14, "conso_2025_m3": 4200000, "delai_reclamation_j": 6.2, }, }, "directions": DIRECTIONS, "parametres": P, "zones": ZONES, "industriels": INDUSTRIELS, "prelevements": PRELEVEMENTS, "resultats": RESULTATS, "enquetes": ENQUETES, "reclamations": RECLAMATIONS, "consommations": CONSOMMATIONS, "dossiers": DOSSIERS, "documents": DOCUMENTS, "journal": JOURNAL, } OUT.write_text(json.dumps(STATE, ensure_ascii=False, indent=2), encoding="utf-8") print(f"Wrote {OUT} | {len(DIRECTIONS)} directions | {len(P)} params | {len(INDUSTRIELS)} industriels | {len(PRELEVEMENTS)} prelevements | {len(RESULTATS)} resultats") print("Codes:", ", ".join(i["code"] for i in INDUSTRIELS))