mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 14:57:09 +00:00
V1.23.28 Mise à jour de la BDD des entreprises et stratégies
This commit is contained in:
@@ -3,3 +3,6 @@ claude_code/
|
||||
|
||||
# Cache bytecode Python, regenere automatiquement.
|
||||
scripts/__pycache__/
|
||||
|
||||
# Identifiants (cles API, mots de passe) -- jamais versionnes.
|
||||
scripts/.env
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
REORGANISATION DE LA STRUCTURATION DES DONNEES ENTREPRISES
|
||||
|
||||
1. Tables mutualisées à créer :
|
||||
- Constituer la table des activités principales (mutualisée)
|
||||
- Constituer la table des natures juridiques
|
||||
- Constituer la table des tranches des effectifs salariés
|
||||
|
||||
*** Attention *** Me proposer pour chacun de ces tables une structure et
|
||||
attendre que je valide avant de les remplir de valeurs
|
||||
|
||||
2. Questions préalables et remarques concernant les tables Phase1 :
|
||||
- A quoi correspond ENT_SectionActivite ?
|
||||
- A t on récupéré des entreprises avec plusieurs noms de dirigeant ?
|
||||
- Il est utile de garder les entreprises dans un departement nn qui n'ont pas
|
||||
d'établissement identifié dans le département car cela va permettre de savoir qu'il
|
||||
existe un établisement secondaire dont on n'a pas l'adresse !
|
||||
Il faut pour cela créer un nouveau champ pour différencier Sièges et Filiales
|
||||
- Interrogations concernant le champ JSON :
|
||||
- quid du caractère_employeur ?
|
||||
- quid du statut diffusion ?
|
||||
- quid de complements{} ?
|
||||
- quid de matching etablissements ?
|
||||
- quid de epci ?
|
||||
- quid de liste_rge = liste rouge ?
|
||||
- quid de liste idcc ?
|
||||
- quid de liste finess ?
|
||||
- quid de liste uai ?
|
||||
|
||||
*** Important *** Attendre que toutes ces questions soient traitées avant de passer
|
||||
à la phase suivante car les réponses peuvent avoir un impact sur
|
||||
l'évolution de structure des tables Phase1
|
||||
|
||||
3. Modifications à apporter aux tables Phase1
|
||||
- Constituer les champs suivants à partir éventuellement du JSON siege{} :
|
||||
- ENT_AdresseSiegeLig1 = numero_voie+", "+type_voie+" "+libellé voie
|
||||
*** Attention ** je tiens à ce que la ligne 1 soit bien écrite "15, rue des Chataignes"
|
||||
Cette petite différence a une utilité pour moi
|
||||
- ENT_AdresseSiègeLig2 = complement_adresse
|
||||
- Renommer Ent_CodePostal en ENT_AdresseSiegeCP
|
||||
- Renommer ENT_Commune en ENT_AdresseSiegeCommune
|
||||
- Renommer ENT_CodeCommune en ENT_AdresseSiegeCodeCommune
|
||||
- Ajouter ENT_AdresseSiegeCedex et y mettre le champ JSON
|
||||
- Renommer ENT_Departement en ENT_AdresseSiegeDepartement
|
||||
- Renommer ENT_Region en ENT_AdresseSiegeRegion
|
||||
- Supprimer le champ ENT_AdresseSiege
|
||||
- Créer un champ ENT_CapitalSocial
|
||||
- ENT_FinanceAnnee, ENT_FinanceCa, ENT_FinanceResultatNet doivent être reportés dans Phase3 et après vérificztion suppression de ces champs
|
||||
- ENT_CanalProspectio, ENT_Phase2, ENT_ScoreCiblage à supprimer
|
||||
- ENT_Notes est il un champ récupéré ? Si non le supprimer
|
||||
- Ajouter le champ ENT_Statut type enum "Siège" "Établissement Secondaire"
|
||||
Quand on récupèrera en extraction ce type d'entreprise, on ne récupèrera pas l'adresse
|
||||
ce qui me laisse penser que ces entreprises vont souvent échapper à certains critères de recherche
|
||||
qu'il faudra prévoir (établissements secondaires dont on ne connait pas l'adresse)
|
||||
- A récupérer dans le JSON (si ce n'est pas déjà le cas) :
|
||||
- nombre d'établissements ouverts
|
||||
- numéro de TVA (très important)
|
||||
- autres champs en fonction des réponses du point 2
|
||||
|
||||
4. Déplacement des champs JSON
|
||||
- Supprimer la table Phase2
|
||||
- Recréer la table Phase2 en ajoutant le champ ENT_SIREN et ENT_ReponseBruteJson
|
||||
- Déplacer les champs ENT_ReponseBruteJson de Phase1 vers Phase2
|
||||
- Supprimer après vérificaztion du bon déplacement le champ ENT_ReponseBruteJson de Phase1
|
||||
- Effectuer ce traitement pour tous les départements
|
||||
|
||||
5. Modification de la table ENT_Communes
|
||||
- Ajouter le champ COM_ETS
|
||||
- Y reporter le nombre d'établissements secondaires
|
||||
|
||||
6. Modification de la table ENT_Departements
|
||||
- Ajouter le champ DEP_NbCommunes
|
||||
- Ajouter les champs DEP_EI, DEP_TPE, DEP_PME, DEP_ETI, DEP_GE, DEP_ETS
|
||||
- Effectuer les calculs
|
||||
|
||||
7. Création de la table des régions ENT_Regions
|
||||
- Champ REG_Code
|
||||
- Champ REG_NbDepartements
|
||||
- Champ REG_NbCommunes
|
||||
- Champ REG_NbEntreprises
|
||||
- Ajouter les champs REG_EI, REG_TPE, REG_PME, REG_ETI, REG_GE, REG_ETS
|
||||
- Effectuer les calculs
|
||||
|
||||
8. Faire le point sur les procédures de mise à jour de toutes ces tables
|
||||
- Revoir les procédures de mise à jour de Phase1, Phase3
|
||||
- Organiser leur traitement : la nuit, étendue du traitement...
|
||||
- Etudier la mise à jour du capital social de chaque entreprise
|
||||
|
||||
9. Création d'une table mutualisée dans la BDD ENTREPRISES
|
||||
Cette tabel aura pour objectif de servir de recensement des temps et coûts de traitement
|
||||
des opérations automatiques réalisées par ASPIC pour qualifier des entreprises et cepour
|
||||
l'ensemble des utilisateurs, en faisant en sorte de pouvoir faire ces analyses par type
|
||||
d'entreprise. Je suis presque certain que cera difficile de qualifier des EI que des PME !
|
||||
On ne va pas aller jusqu'à identifier les différences de ces releés par région, mais je
|
||||
veux que ASPIC soit capable d'annoncer, par la voix de Viktor que "pour qualifier cette
|
||||
sélecton de xx entreprises, j'estime que je vais mettre xx heures, que je vais réussir à
|
||||
en qualifier xx (yy%) et cela va coûter xx€ sur votre crédit de xx€. Confirmez vous le
|
||||
lancement de la qualification ?"
|
||||
On lancera ensuite plusieurs tests de qualification pour commencer à alimenter cette tabel
|
||||
mais cela fera l'objet d'un futur chantier !
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
REORGANISATION DE LA STRUCTURATION DES DONNEES ENTREPRISES
|
||||
|
||||
1. Tables mutualisées à créer :
|
||||
- Constituer la table des activités principales (mutualisée)
|
||||
- Constituer la table des natures juridiques
|
||||
- Constituer la table des tranches des effectifs salariés
|
||||
|
||||
*** Attention *** Me proposer pour chacun de ces tables une structure et
|
||||
attendre que je valide avant de les remplir de valeurs
|
||||
|
||||
2. Questions préalables et remarques concernant les tables Phase1 :
|
||||
- A quoi correspond ENT_SectionActivite ==> une section NAF. Utile ?
|
||||
- A t on récupéré des entreprises avec plusieurs noms de dirigeant ? ==> Oui
|
||||
- Il est utile de garder les entreprises dans un departement nn qui n'ont pas
|
||||
d'établissement identifié dans le département car cela va permettre de savoir qu'il
|
||||
existe un établisement secondaire dont on n'a pas l'adresse !
|
||||
Il faut pour cela créer un nouveau champ pour différencier Sièges et Filiales
|
||||
- Interrogations concernant le champ JSON :
|
||||
- quid du caractère_employeur ?
|
||||
- quid du statut diffusion ?
|
||||
- quid de complements{} ?
|
||||
- quid de matching etablissements ?
|
||||
- quid de epci ?
|
||||
- quid de liste_rge = liste rouge ?
|
||||
- quid de liste idcc ?
|
||||
- quid de liste finess ?
|
||||
- quid de liste uai ?
|
||||
|
||||
*** Important *** Attendre que toutes ces questions soient traitées avant de passer
|
||||
à la phase suivante car les réponses peuvent avoir un impact sur
|
||||
l'évolution de structure des tables Phase1
|
||||
|
||||
3. Modifications à apporter aux tables Phase1
|
||||
- Constituer les champs suivants à partir éventuellement du JSON filiale{} :
|
||||
- ENT_AdresseLig1 = numero_voie+", "+type_voie+" "+libellé voie
|
||||
*** Attention ** je tiens à ce que la ligne 1 soit bien écrite "15, rue des Chataignes"
|
||||
Cette petite différence a une utilité pour moi
|
||||
- ENT_AdresseLig2 = complement_adresse
|
||||
- Renommer Ent_CodePostal en ENT_AdresseCP
|
||||
- Renommer ENT_Commune en ENT_AdresseCommune
|
||||
- Renommer ENT_CodeCommune en ENT_AdresseCodeCommune
|
||||
- Ajouter ENT_AdresseCedex et y mettre le champ JSON
|
||||
- Renommer ENT_Departement en ENT_AdresseDepartement
|
||||
- Renommer ENT_Region en ENT_AdresseRegion
|
||||
- Ne pas conserver les détails de l'adresse du siège
|
||||
- Ajouter le champ ENT_SiegeSIREN avec le numéro SIREN du siège
|
||||
- Ajouter le champ ENT_SiegeDept avec le numéro de département du siège
|
||||
- Récupérer tous les champs idcc, finess, uai, epci, ess, rge, qualiopi, association...
|
||||
- Supprimer le champ ENT_AdresseSiege
|
||||
- Créer un champ ENT_CapitalSocial
|
||||
- ENT_FinanceAnnee, ENT_FinanceCa, ENT_FinanceResultatNet doivent être reportés dans Phase3 et après vérificztion suppression de ces champs
|
||||
- ENT_CanalProspectio, ENT_Phase2, ENT_ScoreCiblage, ENT_StatutQualification à supprimer
|
||||
- ENT_Notes est il un champ récupéré ? Si non le supprimer
|
||||
- A récupérer dans le JSON (si ce n'est pas déjà le cas) :
|
||||
- nombre d'établissements ouverts
|
||||
- numéro de TVA (très important)
|
||||
- autres champs en fonction des réponses du point 2
|
||||
- les noms des dirigeants peuvent être intéressants pour les TPE, PME car ce seront les contacts probables
|
||||
à conserver donc... de quelle façon ?
|
||||
|
||||
4. Déplacement des champs JSON
|
||||
- Supprimer la table Phase2
|
||||
- Créer la table Phase4
|
||||
en ajoutant le champ ENT_SIREN et ENT_ReponseBruteJson
|
||||
- Déplacer les champs ENT_ReponseBruteJson de Phase1 vers Phase2
|
||||
- Supprimer après vérificaztion du bon déplacement le champ ENT_ReponseBruteJson de Phase1
|
||||
- Effectuer ce traitement pour tous les départements
|
||||
|
||||
5. Modification de la table ENT_Communes
|
||||
- Ajouter le champ COM_ETS
|
||||
- Y reporter le nombre d'établissements secondaires
|
||||
|
||||
6. Modification de la table ENT_Departements
|
||||
- Ajouter le champ DEP_NbCommunes
|
||||
- Ajouter les champs DEP_EI, DEP_TPE, DEP_PME, DEP_ETI, DEP_GE, DEP_ETS
|
||||
- Effectuer les calculs
|
||||
|
||||
7. Création de la table des régions ENT_Regions
|
||||
- Champ REG_Code
|
||||
- Champ REG_NbDepartements
|
||||
- Champ REG_NbCommunes
|
||||
- Champ REG_NbEntreprises
|
||||
- Ajouter les champs REG_EI, REG_TPE, REG_PME, REG_ETI, REG_GE, REG_ETS
|
||||
- Effectuer les calculs
|
||||
|
||||
8. Faire le point sur les procédures de mise à jour de toutes ces tables
|
||||
- Revoir les procédures de mise à jour de Phase1, Phase3
|
||||
- Organiser leur traitement : la nuit, étendue du traitement...
|
||||
- Etudier la mise à jour du capital social de chaque entreprise avec fichiers PDF
|
||||
même si ils ne sont pas mis à jour comme annoncé par ailleurs
|
||||
|
||||
9. Création d'une table mutualisée dans la BDD ENTREPRISES
|
||||
Cette tabel aura pour objectif de servir de recensement des temps et coûts de traitement
|
||||
des opérations automatiques réalisées par ASPIC pour qualifier des entreprises et cepour
|
||||
l'ensemble des utilisateurs, en faisant en sorte de pouvoir faire ces analyses par type
|
||||
d'entreprise. Je suis presque certain que cera difficile de qualifier des EI que des PME !
|
||||
On ne va pas aller jusqu'à identifier les différences de ces releés par région, mais je
|
||||
veux que ASPIC soit capable d'annoncer, par la voix de Viktor que "pour qualifier cette
|
||||
sélecton de xx entreprises, j'estime que je vais mettre xx heures, que je vais réussir à
|
||||
en qualifier xx (yy%) et cela va coûter xx€ sur votre crédit de xx€. Confirmez vous le
|
||||
lancement de la qualification ?"
|
||||
On lancera ensuite plusieurs tests de qualification pour commencer à alimenter cette tabel
|
||||
mais cela fera l'objet d'un futur chantier !
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -467,3 +467,63 @@ Restait un point sans case évidente : la dimension relationnelle envers l'équi
|
||||
**Statut.** Migration du 56 terminée et vérifiée (167 468 lignes, structure `ENT_`). Extraction du 29 en cours au moment de la rédaction ; 48 traité comme département témoin pour les tests (21 859 entreprises). Restent à traiter : 22 et 35 (Bretagne), puis 17 et 64 évoqués pour l'usage "déplacement personnel". `ENT_Communes` alimentée pour 56/48/29. La synchronisation quotidienne (INPI/INSEE) reste à concevoir et construire — rien engagé au-delà de l'identification des bons endpoints. Aucune ligne de code applicatif ASPIC touchée ce soir : tout le travail décrit ici vit hors du dépôt (base `ENTREPRISES` + script autonome), toujours volontairement tenu à l'écart de l'application elle-même.
|
||||
|
||||
`<code>` n'avance pas dans cette section (aucune ligne du dépôt applicatif ASPIC touchée) ; `<conception>` avance de 24 à 25 — nouvelle version **V1.23.25**.
|
||||
|
||||
## 26. Backfill historique des comptes annuels, dimensionnement disque national, et un incident sérieux (12-13 septembre 2026)
|
||||
|
||||
**Origine.** Suite directe de la section 25 : les 4 départements bretons extraits, restait à peupler Phase3 avec l'historique complet des comptes annuels (2017 à aujourd'hui), pas seulement la dernière année déjà captée à l'extraction.
|
||||
|
||||
**Traduire la liasse fiscale brute en CA/résultat net — le vrai travail de cette session.** L'endpoint INPI `/api/bilans-saisis` ne renvoie pas un CA/résultat net direct mais une liasse fiscale complète (codes CERFA type "FJ", "HN"...), dont la signification dépend du type de bilan (`codeTypeBilan` : C, S, K, AC, AS, B). Table de correspondance établie à partir de la documentation officielle INPI (v5, juin 2025) puis **vérifiée empiriquement sur un vrai bilan (EDF, SIREN 552081317)** pour confirmer la bonne colonne (m3 = exercice en cours quand elle existe, sinon m1). Bug détecté et corrigé en cours de route : une même entreprise dépose souvent **à la fois** ses comptes individuels (C) et consolidés de groupe (K) pour la même année — sans règle de priorité, l'un écrasait l'autre au hasard de l'ordre de traitement. Priorité retenue : toujours préférer les comptes propres de l'entité (C/AC/AS/S) à un chiffre de groupe consolidé (K), qui ne représente plus l'entreprise précise. Les bilans en confidentialité non publique ne sont jamais exploités, par respect du choix déclaré par l'entreprise. Script autonome : `src/scripts/inpi_bilans_backfill.py`, surnommé **BFH** ("backfill historique") dans les échanges avec Jean-Marc — désormais systématiquement enchaîné après `sirene_extract.py` pour tout nouveau département, sans qu'il ait à le redemander (Phase2 reste exclue de cet automatisme, elle demeure un traitement manuel à part).
|
||||
|
||||
**Tables de pilotage ajoutées à la demande de Jean-Marc** : `ENT_Departements` (un département géré = une ligne, avec compteur d'entreprises mis en cache pour éviter des `COUNT(*)` répétés sur des tables qui grossissent) et `ENT_Config` (clé/valeur générique pour les réglages globaux, ex. date de la prochaine synchro quotidienne des comptes annuels, qui n'est pas filtrable par département). Remplace l'idée initiale de Claude (découvrir les départements via `SHOW TABLES`) — Jean-Marc a jugé une vraie table plus simple à interroger depuis ASPIC, avec raison.
|
||||
|
||||
**Dimensionnement national anticipé par Jean-Marc, avant d'avoir mal.** Question posée de façon proactive : sachant qu'il y a ~8,5M d'entreprises en France (chiffre INSEE cité par Jean-Marc), la base tiendra-t-elle le choc ? Calcul fait sur les tailles réelles mesurées (~7,3 Ko/entreprise en moyenne sur 5 départements) : projection nationale ≈ 70 Go, soit 36-40% du disque du VPS (193 Go, 154 Go libres à ce moment) — un chiffre que Jean-Marc a justement qualifié de "pas négligeable", refusant de le balayer d'un revers de main. Jean-Marc a lui-même relevé que la région parisienne (75 : 484 011 entreprises, 92 : 265 707, chiffres INSEE vérifiés) serait un point chaud disproportionné, pas une simple extrapolation linéaire depuis la Bretagne. Solution appliquée : compression native InnoDB (`ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8`) sur la table Phase1 (dominée par le JSON brut de l'API, qui compresse très bien) — gain réel mesuré de **-60%** sur les 5 tables existantes, ramenant la projection nationale à ~28-30 Go (15% du disque). Intégré au gabarit de création de table de `sirene_extract.py`, donc acquis automatiquement pour tous les départements à venir sans y repenser.
|
||||
|
||||
**Incident sérieux dans la nuit : le BFH a fait tomber le serveur en OOM.** Le script a été tué par le noyau après avoir atteint 9,33 Go de RAM — et le OOM killer a également tué **systemd lui-même** au passage (le serveur s'est remis seul, sans perte de service durable, 10 jours d'uptime conservés). Cause identifiée : la boucle de pagination par curseur (`searchAfter`) de l'appel `/api/bilans-saisis` n'avait aucun garde-fou — un lot bloqué (curseur qui n'avance plus, ou volume de résultats anormal pour un gros groupe) pouvait boucler en accumulant des résultats en mémoire indéfiniment, sans jamais lever d'erreur propre ni s'arrêter. Corrigé par un double garde-fou : plafond de 100 pages par lot, et détection explicite d'un curseur qui stagne (comparaison avec la valeur précédente). **Nouvelle règle de méthode actée pour la suite : tout lancement d'un script de récupération de données volumineuses doit désormais être encadré par `ulimit -v` (4 Go utilisé ici) avant le `nohup`, pour qu'un futur bug fasse planter le script, jamais le serveur entier.** Aucune perte de données : la conception en `UPSERT` (jamais un `INSERT` seul) fait que reprendre depuis zéro après un crash ne recrée aucun doublon, juste un peu de travail réseau redondant.
|
||||
|
||||
**Statut.** BFH relancé avec succès après correctif (mémoire redescendue de plusieurs Go à 62 Mo au même point de progression — le vrai bug semble bien corrigé, pas seulement contenu par le plafond de sécurité). Jean-Marc parti se coucher avec la suite actée pour cette nuit, sans supervision : une fois le BFH du 56 terminé, extraction + BFH enchaînés pour 44, 49, 85, 17, 40, 64 et 65, chaque étape protégée par le même `ulimit`. Mise à jour du dépôt remise au lendemain par Jean-Marc lui-même, comme toujours.
|
||||
|
||||
`<code>` n'avance pas dans cette section (aucune ligne du dépôt applicatif ASPIC touchée — tout le travail vit dans `src/scripts/` et la base `ENTREPRISES`) ; `<conception>` avance de 25 à 26 — nouvelle version **V1.23.26**.
|
||||
|
||||
## 27. Statistiques par commune, premières esquisses de carte de prospection géographique, et un correctif de fond sur le repli NAF (13 septembre 2026)
|
||||
|
||||
**Suite de la nuit du 12 au 13.** La chaîne sans supervision (44, 49, 85, 17, 40, 64, 65) s'est terminée sans incident, `ulimit -v` et garde-fou de pagination tenant leurs promesses. Une seconde chaîne a été lancée dans la foulée pour 31, 32, 33, 46, 47, 09, 13 et 66 (Occitanie/PACA), avec un décalage de départ pour ne jamais faire tourner deux extractions Sirene en même temps.
|
||||
|
||||
**Nouveau champ `ENT_CodeCommune` sur Phase1**, backfillé sans nouvel appel API (déjà présent dans le JSON brut stocké de chaque entreprise) sur les 7 départements de la première chaîne — nécessaire pour la fonctionnalité suivante.
|
||||
|
||||
**Statistiques par taille d'entreprise, par commune.** À la demande de Jean-Marc : cinq nouveaux champs sur `ENT_Communes` (`COM_EI`, `COM_TPE`, `COM_PME`, `COM_ETI`, `COM_GE`), alimentés par un script quotidien dédié (`ent_communes_stats.py`), pour que le futur module de recherche géographique n'ait jamais à compter en temps réel sur des tables Phase1 qui grossissent. Règle de classification : `ENT_CategorieEntreprise` prioritaire quand l'INSEE la renseigne (GE/ETI/PME) ; sinon répartition EI/TPE selon le premier chiffre de `ENT_NatureJuridique` (entrepreneur individuel = '1'). Deux bugs trouvés et corrigés avant mise en production : `SUM()` renvoie `NULL` (pas 0) sur un ensemble vide, faussant l'agrégat d'une commune sans aucune entreprise d'une catégorie — corrigé par `COALESCE(..., 0)` ; et surtout, une commune pouvait voir ses compteurs écrasés par un département qui ne la possède pas réellement (établissement secondaire présent dans la table Phase1 d'un département voisin) — corrigé en restreignant le calcul aux communes réellement rattachées au département traité via une jointure sur `ENT_Communes`, révélant au passage un conflit de collation (`utf8mb4_unicode_ci` vs `utf8mb4_0900_ai_ci`) entre tables créées à des moments différents, réglé par un `COLLATE` explicite dans la jointure.
|
||||
|
||||
**Première brique d'écran : la carte de prospection par géographie personnelle**, idée actée en section 25 (l'isochrone IGN), construite ce jour sous forme de prototypes interactifs (Claude Artifacts) plutôt que dans l'application : une commune de départ au centre, les communes accessibles en 30 minutes de route autour (isochrone réel + position géographique relative, projection locale corrigée du cosinus de la latitude), chacune avec son temps de trajet réel (API IGN `itineraire`) et ses effectifs EI/TPE/PME/ETI/GE réels tirés de `ENT_Communes`. Trois itérations avec retour de Jean-Marc avant d'aboutir à une version jugée satisfaisante : espacement des cartes pour éviter le chevauchement (algorithme de répulsion par paires, position géographique réelle conservée via un point + ligne de rappel quand une carte est déplacée pour la lisibilité), puis remplacement de la navigation par ascenseur natif (peu fiable dans le rendu de la fenêtre d'Artifact à certains niveaux de zoom navigateur) par un cliquer-glisser JavaScript, au clic gauche **et** au clic droit — Jean-Marc ayant signalé une habitude de navigation au clic droit. Quatre versions produites : Lorient (démonstration initiale), Lanester (ville de résidence de Jean-Marc, test de cohérence des distances), puis Concarneau et Beuzec-Cap-Sizun (Finistère) à la demande de Jean-Marc pour deux amis, comme simple partage d'un lien — pas encore une fonctionnalité ASPIC, mais une preuve que la donnée collectée a une valeur de démonstration immédiate.
|
||||
|
||||
**Correctif de fond sur le repli NAF à 3 niveaux — la vraie panne de la journée.** Le 3e niveau de repli (par code NAF précis), ajouté le 12 septembre pour débloquer Rennes (section 26 — un seul cas rencontré alors, section L), s'est révélé mal conçu à l'échelle : au lieu de ne tester que les codes NAF appartenant à la section en cours, il retestait les 732 codes en entier **à chaque section qui restait plafonnée**. Sur une grande ville où plusieurs sections dépassent le plafond simultanément (Toulouse : au moins 6 sections concernées), ce défaut a bloqué l'extraction du département 31 pendant plusieurs heures sans jamais planter ni progresser de façon visible — seule la répétition suspecte des mêmes codes (68.20A/68.20B, immobilier) à chaque section a permis de repérer le problème. Corrigé par une table de correspondance division→section NAF (standard, 21 sections, vérifiée exhaustive sur les 732 codes), limitant désormais le 3e niveau aux seuls codes réellement concernés par la section (6 codes pour la section L, contre 732 avant). Département 31 relancé depuis le début après correctif (pas de mécanisme de reprise par commune dans le script) : Toulouse traitée en quelques minutes au second passage, chaîne 2 poursuivie normalement.
|
||||
|
||||
**Cap symbolique franchi en cours de journée : plus de 2 millions d'entreprises cumulées** (2 227 587 exactement, sur 15 départements traités, le 33 encore en cours) — 31 (Haute-Garonne, avec Toulouse) est à lui seul le département le plus lourd traité jusqu'ici (312 359 entreprises).
|
||||
|
||||
**Premières réflexions sur la refonte du module Contacts, notées mais pas construites.** Jean-Marc a acté un changement de sens par rapport à la conception initiale des contacts : le lien entreprise↔contact ne sera plus exclu multiple (une entreprise peut avoir plusieurs contacts, un contact peut être lié à plusieurs entreprises), et la qualification (statut, score, canal) portera sur l'**entreprise**, pas sur le contact — la fiche contact ne conservant de l'entreprise que ce qui sert à garder le lien. Trois étapes de prospection distinguées : qualification des entreprises sélectionnées, recherche du ou des contacts, démarche commerciale classique — avec la précision que toute prospection ne partira pas nécessairement d'une qualification d'entreprise (d'autres approches resteront possibles). En préparant les écrans, Jean-Marc a demandé le détail du contenu du JSON brut Sirene stocké (structure décrite : `siege`, `dirigeants`, `finances`, `complements`) et relevé l'absence du capital social, confirmée après vérification (absent de cette API, disponible seulement côté INPI RNE, jamais interrogé à ce jour) — Jean-Marc a d'ores et déjà indiqué que ce champ relèverait de la qualification locale (donc jamais mutualisée), cohérent avec la séparation Phase1/Phase2 actée section 25. Sujet remis à plus tard dans son ensemble, quand ce chantier sera formellement ouvert.
|
||||
|
||||
**Statut.** 15 départements avec extraction et historique financier complets (17, 22, 29, 35, 40, 44, 48, 49, 56, 64, 65, 85 + 31, 32 achevés dans la chaîne 2, 33 en cours). Restent 46, 47, 09, 13, 66 en extraction puis BFH pour l'ensemble des 8 nouveaux départements. `ent_communes_stats.py` opérationnel mais pas encore automatisé quotidiennement (à brancher sur n8n avec la synchronisation incrémentale, toujours non construite). Quatre prototypes de carte de prospection publiés en Artifacts, partagés par Jean-Marc à deux amis — aucune intégration à l'application elle-même.
|
||||
|
||||
`<code>` n'avance pas dans cette section (tout le travail vit dans `src/scripts/`, la base `ENTREPRISES`, et des prototypes hors dépôt) ; `<conception>` avance de 26 à 27 — nouvelle version **V1.23.27**.
|
||||
|
||||
## 28. Suspension du BFH, découverte des arrondissements PLM, extraction France entière et refonte de la feuille de route ENTREPRISES (14-15 septembre 2026)
|
||||
|
||||
**Deux bugs de fond supplémentaires trouvés sur le BFH, le lendemain du précédent.** Le curseur de pagination de `bilans-saisis` utilisait le SIREN du dernier résultat comme `searchAfter` — or un SIREN a souvent plusieurs bilans (plusieurs années), donc revient plusieurs fois dans les résultats ; le garde-fou anti-boucle de la veille prenait ça à tort pour un blocage et abandonnait le lot en cours, perdant des bilans déjà partiellement récupérés (~14% des lots du département 31 touchés). Vérifié empiriquement que le champ `id` (identifiant unique par bilan, présent dans chaque réponse) donne une pagination propre sans doublon — corrigé. Puis, une fois relancé, l'API INPI a commencé à renvoyer des 401 (jeton expiré en cours d'exécution) après plusieurs heures d'appels continus — le script retentait 8 fois avec le même jeton mort, ce qui ne pouvait jamais réussir. Corrigé par une reconnexion automatique (`auth` devient un dict mutable partagé, se régénère tout seul sur un 401).
|
||||
|
||||
**Puis un vrai mur, cette fois non contournable par du code : un quota INPI dépassé** (`"errorCode":"quota_exceeded"`, `"webserviceCode":"QUOTA_SERVICE"`, confirmé par inspection directe du corps de réponse HTTP 429). Contrairement à une limitation de débit passagère, retenter ne sert à rien tant que le quota ne s'est pas régénéré (vraisemblablement journalier). Correctif appliqué par prudence (une `QuotaExceededError` dédiée fait interrompre immédiatement tout le traitement plutôt que de s'acharner lot après lot), mais **décision de Jean-Marc, plus radicale et plus sage : suspendre entièrement le BFH pour l'instant**, risque de bannissement du compte INPI jugé disproportionné par rapport au bénéfice — l'historique financier pourra être rattrapé "petit à petit dans les mois à venir", l'essentiel étant d'abord de disposer de l'identité de toutes les entreprises françaises pour commencer à travailler sur la structure des données et les écrans.
|
||||
|
||||
**Découverte majeure et indépendante, sur l'extraction Sirene elle-même : Paris, Lyon et Marseille (PLM) étaient massivement sous-captées.** Repéré à l'œil par Jean-Marc, qui a trouvé étonnant que le département 33 (Gironde, 416 980 entreprises) dépasse le 13 (Bouches-du-Rhône, alors 330 238) alors que Marseille est économiquement plus lourde que Bordeaux. Vérifié : dans les données Sirene, une entreprise marseillaise n'est quasiment jamais rattachée au code commune englobant (13055, ~10 résultats seulement) mais à son arrondissement (13201 à 13216) — or la liste des communes utilisée pour l'extraction ne renvoyait que le code englobant, jamais les 16 arrondissements, chacun pourtant déjà plafonné ou proche du plafond de 10 000 à lui seul. Corrigé (`sirene_extract.py` détecte désormais les trois villes à arrondissements et interroge `geo.api.gouv.fr?type=arrondissement-municipal` à la place du code englobant), `ENT_Communes` corrigée à la main pour le 13 (13055 supprimée, ses 16 arrondissements ajoutés), réextraction du 13 replanifiée en fin de chaîne. Paris et Lyon n'avaient pas encore été traités à ce stade, donc le correctif s'applique nativement dès leur premier passage.
|
||||
|
||||
**Décision de méthode pour la suite : terminer l'aspiration avec la méthode Sirene actuelle jusqu'au bout, sans la mélanger à une autre source en cours de route** (pour ne pas risquer d'hétérogénéiser les données), **mais reconcevoir entièrement la stratégie de mise à jour a posteriori**. Chaîne dédiée relancée (extraction seule, aucun appel INPI) pour l'intégralité de la France restante — 81 départements, métropole **et DOM-TOM inclus** (Jean-Marc : "Ne te limite pas à la métropole ! J'ai des amis à la Réunion, j'y passerai bien volontiers du temps !"), La Réunion placée en tête de liste pour cette raison personnelle. Rythme observé : environ 1h/département pour un département rural, plusieurs heures pour une grande ville nécessitant le repli NAF (Nice : 3h05). **Statut à la rédaction : 55 départements extraits, 7 036 958 entreprises** (~83% de l'estimation nationale de 8,5M) — la trajectoire confirme l'hypothèse de Jean-Marc que les 24 premiers départements traités (Bretagne puis Occitanie/PACA) étaient disproportionnellement denses, et que la moyenne va continuer de se rapprocher du national à mesure que les départements ruraux restants sont traités.
|
||||
|
||||
**Vérification croisée de plusieurs ratios avec Jean-Marc, à chaque fois confirmée par de vraies mesures plutôt qu'une simple extrapolation théorique** : le nombre d'entreprises (dépassement des 50% national constaté en direct) ; l'espace disque (10,69 Go réels pour 3,63M entreprises à ce moment, soit ~3 133 octets/entreprise compressé, projection nationale ~26,6 Go — cohérente avec l'estimation théorique de la section 26) ; et la proportion d'entrepreneurs individuels, mesurée sur les données déjà collectées plutôt que supposée : **41,3% d'EI (sans capital social), 58,7% de non-EI**, dont 3,27M déjà catégorisées GE/ETI/PME par l'INSEE.
|
||||
|
||||
**Piste ouverte par un ami de Jean-Marc, consultant senior, vérifiée et confirmée réelle : l'INPI met à disposition un export en masse du RNE, en FTP/SFTP, séparé de l'API paginée.** Recherche effectuée (dont un projet open-source `etalab/rncs_worker_api_entreprise` en confirmant l'usage réel) : structure stock + flux quotidien, format CSV, capital social explicitement présent comme champ. Demande d'accès faite par Jean-Marc le 15 septembre, accordée le jour même. Connexion testée (lecture seule) : `ftp://www.inpi.net` (FTP simple, pas du vrai SFTP malgré l'intitulé de la demande — source probable de la première tentative infructueuse de Jean-Marc sous FileZilla, configuré en SFTP). Quatre fichiers trouvés à la racine, deux jeux (formalités/comptes annuels) × deux versions chacun, aucun flux quotidien visible à ce niveau : `stock_RNE_formalites_NIVEAU1_20260320_1400.zip` (15,1 Go, mars 2026 — vraisemblablement la source du capital social), `stock_RNE_comptes_annuels_NIVEAU1_20260320_1400.zip` (4,3 Go). Dernier stock à 6 mois d'écart de la date du jour — jugé acceptable pour le capital social (donnée peu volatile) mais insuffisant en l'état pour les comptes annuels. Identifiants stockés dans `.env` (`INPI_RNE_FTP_*`), rien téléchargé, sujet à approfondir sans urgence.
|
||||
|
||||
**Réouverture et correction consciente d'une décision antérieure sur le capital social.** Jean-Marc était revenu sur ce qu'il avait dit trois jours plus tôt (section 27) : le capital social avait été classé à tort comme donnée de qualification privée par compte, alors que c'est une donnée factuelle de l'entreprise au même titre que le NAF ou l'effectif — elle doit vivre dans `ENTREPRISES`, mutualisée. Jean-Marc a également tranché explicitement pour une exception assumée au principe "jamais d'écriture dans `ENTREPRISES` déclenchée par un utilisateur" : la mise à jour du capital social pourra être initiée par une action utilisateur, précisément parce que c'est une information structurante — la conception du mécanisme (file d'attente, appel synchrone, etc.) reste ouverte, dépendante de limites de l'API RNE non encore connues (débit, temps de réponse), sans urgence vu le lancement commercial d'ASPIC visé "seulement" début 2028.
|
||||
|
||||
**Feuille de route formelle déposée par Jean-Marc** (`src/fdr/1. ENTREPRISES V1.0`), première utilisation de ce nouveau circuit de transmission (dépôt de fichier dans un répertoire dédié plutôt que description en conversation). Relecture complète effectuée à la demande explicite de Jean-Marc ("pour avoir ton retour... pas pour passer à l'action"). Points de retour marquants : découverte, en vérifiant une question de Jean-Marc sur les champs JSON, que `matching_etablissements[]` (jamais exploité) contient probablement l'adresse propre de chaque établissement secondaire ayant fait matcher une recherche — invalidant l'hypothèse de départ de Jean-Marc qu'on ne pourrait pas la connaître, et permettant potentiellement de retrouver un établissement secondaire par ville de recherche ; ~30% des entreprises ont plusieurs dirigeants dans le JSON, un seul capturé actuellement ; signalement d'une collision de nommage évitée de justesse (la feuille de route proposait de recycler le nom "Phase2", déjà porteur d'un sens différent et déjà documenté — Jean-Marc a tranché : `Phase2` disparaît purement et simplement de `ENTREPRISES`, son rôle originel renaît dans la base de chaque utilisateur sous un nom encore provisoire, `ASP_ENTQualif`).
|
||||
|
||||
**Clarification du trombinoscope des collaborateurs ASPIC, à l'occasion d'une coquille apparente.** La feuille de route mentionnait "la voix de Viktor" pour une future compétence de qualification — pas une erreur : les cinq collaborateurs nommés d'ASPIC sont **Matilda, Viktor, Sophia, Léon et Nikita**, et Jean-Marc a confirmé une répartition claire pour au moins deux d'entre eux : **Léon** porte la gestion (dont la garde d'`ENTREPRISES`), **Viktor** porte le commercial (dont la future qualification et son suivi temps/coût/réussite). Viktor n'a pas encore d'interface vocale prévue, "mais pourquoi pas quand je n'aurai plus que ça à faire".
|
||||
|
||||
**Statut.** BFH suspendu partout, à reprendre progressivement et sans urgence dans les mois à venir, avec une stratégie de mise à jour entièrement à reconcevoir (API paginée vs flux RNE en masse). Extraction Sirene seule en cours sans interruption depuis le 14 septembre, 55 départements sur 101 (métropole + DOM-TOM), 7 036 958 entreprises. Feuille de route ENTREPRISES V1.0 relue et commentée, pas encore amendée par Jean-Marc (en cours). Accès FTP RNE obtenu et testé en lecture seule, non exploité.
|
||||
|
||||
`<code>` n'avance pas dans cette section (deux scripts modifiés — `inpi_bilans_backfill.py`, `sirene_extract.py` — mais toujours hors du dépôt applicatif ASPIC lui-même) ; `<conception>` avance de 27 à 28 — nouvelle version **V1.23.28**.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Calcule et stocke, pour chaque commune de ENT_Communes, le nombre d'entreprises
|
||||
actives par categorie de taille (EI, TPE, PME, ETI, GE) -- pre-calcule pour que
|
||||
le futur module de recherche par perimetre geographique n'ait jamais a compter
|
||||
en temps reel sur les tables Entreprises*Phase1 (qui grossissent).
|
||||
|
||||
Regle de classification (une entreprise tombe dans une seule categorie) :
|
||||
- ENT_CategorieEntreprise = 'GE' -> GE
|
||||
- ENT_CategorieEntreprise = 'ETI' -> ETI
|
||||
- ENT_CategorieEntreprise = 'PME' -> PME
|
||||
- categorie NULL et nature juridique = entrepreneur individuel (1er
|
||||
chiffre de ENT_NatureJuridique = '1') -> EI
|
||||
- categorie NULL, tout le reste -> TPE
|
||||
(ENT_CategorieEntreprise, quand renseignee par l'INSEE, est deja fiable et
|
||||
prioritaire ; les entreprises non categorisees par l'INSEE -- la grande
|
||||
majorite -- sont reparties entre EI et TPE selon leur forme juridique.)
|
||||
|
||||
Concu pour tourner une fois par jour (cf ENT_Communes.COM_DateMajStats) : idem
|
||||
potent, recalcule et ecrase les compteurs a chaque execution, jamais un delta.
|
||||
|
||||
Usage :
|
||||
python3 ent_communes_stats.py
|
||||
python3 ent_communes_stats.py --departements 56,29
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def load_env(path):
|
||||
env = {}
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
env[k.strip()] = v.strip()
|
||||
return env
|
||||
|
||||
|
||||
def compute_departement(conn, dep):
|
||||
table1 = f"Entreprises{dep}Phase1"
|
||||
# IMPORTANT : Entreprises<dep>Phase1 contient aussi des entreprises dont le
|
||||
# siege est dans un AUTRE departement (etablissement secondaire dans <dep> --
|
||||
# la "pollution" deja identifiee). On restreint donc explicitement aux
|
||||
# communes qui appartiennent reellement a <dep> (via ENT_Communes), pour
|
||||
# qu'un departement ne puisse jamais ecraser les stats d'une commune qui ne
|
||||
# lui appartient pas avec un sous-ensemble incomplet.
|
||||
sql = f"""
|
||||
SELECT
|
||||
t.ENT_CodeCommune,
|
||||
COALESCE(SUM(t.ENT_CategorieEntreprise = 'GE'), 0) AS ge,
|
||||
COALESCE(SUM(t.ENT_CategorieEntreprise = 'ETI'), 0) AS eti,
|
||||
COALESCE(SUM(t.ENT_CategorieEntreprise = 'PME'), 0) AS pme,
|
||||
COALESCE(SUM(t.ENT_CategorieEntreprise IS NULL AND LEFT(t.ENT_NatureJuridique, 1) = '1'), 0) AS ei,
|
||||
COALESCE(SUM(t.ENT_CategorieEntreprise IS NULL AND
|
||||
(t.ENT_NatureJuridique IS NULL OR LEFT(t.ENT_NatureJuridique, 1) != '1')), 0) AS tpe
|
||||
FROM `{table1}` t
|
||||
INNER JOIN ENT_Communes c
|
||||
ON c.COM_CodeInsee COLLATE utf8mb4_0900_ai_ci = t.ENT_CodeCommune AND c.COM_Departement = %s
|
||||
WHERE t.ENT_CodeCommune IS NOT NULL
|
||||
GROUP BY t.ENT_CodeCommune
|
||||
"""
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, (dep,))
|
||||
rows = cur.fetchall()
|
||||
|
||||
with conn.cursor() as cur:
|
||||
for code_commune, ge, eti, pme, ei, tpe in rows:
|
||||
cur.execute(
|
||||
"UPDATE ENT_Communes SET COM_EI=%s, COM_TPE=%s, COM_PME=%s, COM_ETI=%s, "
|
||||
"COM_GE=%s, COM_DateMajStats=NOW() WHERE COM_CodeInsee=%s",
|
||||
(ei, tpe, pme, eti, ge, code_commune),
|
||||
)
|
||||
log(f" {dep} : {len(rows)} communes mises a jour")
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--departements", default=None,
|
||||
help="Liste separee par des virgules (defaut : tous ceux de ENT_Departements)")
|
||||
ap.add_argument("--env-file", default=os.path.join(os.path.dirname(__file__), ".env"))
|
||||
args = ap.parse_args()
|
||||
|
||||
env = load_env(args.env_file)
|
||||
conn = pymysql.connect(
|
||||
host=env["ENTREPRISES_DB_HOST"], port=int(env.get("ENTREPRISES_DB_PORT", 3306)),
|
||||
user=env["ENTREPRISES_DB_USER"], password=env["ENTREPRISES_DB_PASS"],
|
||||
database=env["ENTREPRISES_DB_NAME"], charset="utf8mb4", autocommit=True,
|
||||
)
|
||||
|
||||
if args.departements:
|
||||
departements = [d.strip() for d in args.departements.split(",") if d.strip()]
|
||||
else:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT DEP_Code FROM ENT_Departements ORDER BY DEP_Code")
|
||||
departements = [row[0] for row in cur.fetchall()]
|
||||
|
||||
log(f"Departements a traiter : {departements}")
|
||||
total = 0
|
||||
try:
|
||||
for dep in departements:
|
||||
total += compute_departement(conn, dep)
|
||||
finally:
|
||||
conn.close()
|
||||
log(f"Termine : {total} communes mises a jour au total")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Backfill historique des comptes annuels (API RNE/INPI, /api/bilans-saisis)
|
||||
dans EntreprisesXXPhase3, pour toutes les entreprises deja connues (une ligne
|
||||
par SIREN+annee, jamais ecrasee -- voir la conception Phase3).
|
||||
|
||||
Contrairement a l'extraction Sirene (sirene_extract.py), cet appel API n'est
|
||||
pas filtrable par departement : on l'interroge par lot de SIREN (max ~300
|
||||
avant de depasser la limite de longueur d'URL du serveur, on utilise 250 par
|
||||
securite), sans limite de date, pour recuperer tout l'historique disponible
|
||||
(comptes annuels dispo depuis 2017 selon l'espace personnel INPI).
|
||||
|
||||
Les donnees saisies sont une liasse fiscale brute (codes CERFA), pas un CA/
|
||||
resultat net direct -- la table TYPE_BILAN_MAPPING ci-dessous traduit les
|
||||
codes pertinents pour chaque type de bilan (C, S, K, AC, AS, B), etablie le
|
||||
12 septembre 2026 a partir de la doc officielle INPI (v5, juin 2025) et
|
||||
verifiee empiriquement sur un vrai bilan (EDF, SIREN 552081317).
|
||||
|
||||
Regle de colonne (verifiee empiriquement) : quand un code liasse a une
|
||||
colonne m3, elle contient le total de l'exercice en cours (m4 = exercice
|
||||
precedent) ; sinon c'est m1 qui est l'exercice en cours.
|
||||
|
||||
Les bilans en confidentialite non publique ne sont pas exploites (CA/
|
||||
resultat net non extraits), par respect du choix de confidentialite declare.
|
||||
|
||||
Usage :
|
||||
python3 inpi_bilans_backfill.py
|
||||
python3 inpi_bilans_backfill.py --departements 56,29
|
||||
|
||||
Identifiants lus depuis .env (ENTREPRISES_DB_*, INPI_USERNAME, INPI_PASSWORD)
|
||||
dans le meme dossier, jamais en argument.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import pymysql
|
||||
|
||||
class QuotaExceededError(Exception):
|
||||
"""Quota de l'API INPI depasse (distinct d'une simple limitation de debit
|
||||
passagere) -- inutile de retenter, il faut arreter et laisser le quota se
|
||||
regenerer (journalier selon toute vraisemblance) avant de reprendre."""
|
||||
|
||||
|
||||
API_LOGIN = "https://registre-national-entreprises.inpi.fr/api/sso/login"
|
||||
API_BILANS_SAISIS = "https://registre-national-entreprises.inpi.fr/api/bilans-saisis"
|
||||
BATCH_SIZE = 250 # 300 confirme fonctionnel, marge de securite prise
|
||||
PAGE_SIZE = 100
|
||||
RATE_DELAY = 0.2
|
||||
|
||||
# code liasse -> [codes a sommer pour le CA] / [codes a sommer pour le resultat net]
|
||||
TYPE_BILAN_MAPPING = {
|
||||
"C": {"ca": ["FJ"], "rn": ["HN"]},
|
||||
"S": {"ca": ["209", "210", "214", "215", "217", "218"], "rn": ["310"]},
|
||||
"K": {"ca": ["FJ"], "rn": ["R6"]},
|
||||
"AC": {"ca": ["FP"], "rn": ["HO"]},
|
||||
"AS": {"ca": ["EA", "EB", "EC", "ED", "EE"], "rn": ["FJ"]},
|
||||
"B": {"ca": [], "rn": ["R3"]},
|
||||
}
|
||||
|
||||
# Priorite quand plusieurs bilans existent pour le meme SIREN+annee (frequent :
|
||||
# une societe mere depose a la fois ses comptes individuels et consolides).
|
||||
# On privilegie systematiquement les comptes de l'entite elle-meme (C, AC, AS,
|
||||
# S) a un chiffre de groupe consolide (K) qui ne represente plus la societe
|
||||
# precise -- et B (banque) en tout dernier recours seulement.
|
||||
TYPE_PRIORITY = {"C": 0, "AC": 0, "AS": 1, "S": 2, "K": 3, "B": 4}
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def load_env(path):
|
||||
env = {}
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
env[k.strip()] = v.strip()
|
||||
return env
|
||||
|
||||
|
||||
def http_get_json(url, auth, retries=8):
|
||||
# "auth" est un dict mutable {"token", "username", "password"} plutot qu'un
|
||||
# jeton brut, pour pouvoir se reconnecter tout seul sur un 401 (jeton perime
|
||||
# en cours de traitement, constate le 14/09/2026 sur une execution de plusieurs
|
||||
# heures -- l'ancien code retentait 8 fois avec le meme jeton mort, ce qui ne
|
||||
# pouvait jamais reussir, et perdait tous les lots restants du departement).
|
||||
for attempt in range(retries):
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {auth['token']}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.load(resp)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
log(" !! 401 Unauthorized -- jeton probablement expire, reconnexion")
|
||||
auth["token"] = login(auth["username"], auth["password"])
|
||||
continue
|
||||
if e.code == 429:
|
||||
# Deux cas bien distincts derriere le meme code HTTP 429, constates
|
||||
# le 14/09/2026 : une limitation de debit passagere (retentable avec
|
||||
# patience) et un vrai quota de service depasse (corps de reponse
|
||||
# {"errorCode":"quota_exceeded","webserviceCode":"QUOTA_SERVICE"} --
|
||||
# la, retenter ne sert a rien, il faut arreter tout de suite. Avant
|
||||
# cette distinction, le script s'est acharne des heures a retenter
|
||||
# un quota epuise, perdant lot apres lot pour rien.
|
||||
try:
|
||||
body = json.loads(e.read())
|
||||
except Exception:
|
||||
body = {}
|
||||
if body.get("errorCode") == "quota_exceeded":
|
||||
raise QuotaExceededError(body.get("message", "quota INPI depasse"))
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
retry_after = e.headers.get("Retry-After") if e.headers else None
|
||||
wait = int(retry_after) if retry_after and retry_after.isdigit() else min(10 * (2 ** attempt), 300)
|
||||
log(f" !! 429 Too Many Requests -- pause de {wait}s avant nouvelle tentative "
|
||||
f"({attempt + 1}/{retries})")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
log(f" retry apres erreur reseau ({e})")
|
||||
time.sleep(2 + attempt)
|
||||
except (urllib.error.URLError, TimeoutError) as e:
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
log(f" retry apres erreur reseau ({e})")
|
||||
time.sleep(2 + attempt)
|
||||
|
||||
|
||||
def login(username, password):
|
||||
req = urllib.request.Request(
|
||||
API_LOGIN,
|
||||
data=json.dumps({"username": username, "password": password}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
return json.load(resp)["token"]
|
||||
|
||||
|
||||
def get_liasse_value(pages, code):
|
||||
for page in pages:
|
||||
for l in page.get("liasses", []):
|
||||
if l.get("code") == code:
|
||||
raw = l.get("m3") if l.get("m3") not in (None, "") else l.get("m1")
|
||||
if raw in (None, ""):
|
||||
continue
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def sum_codes(pages, codes):
|
||||
if not codes:
|
||||
return None
|
||||
total = 0
|
||||
found_any = False
|
||||
for code in codes:
|
||||
v = get_liasse_value(pages, code)
|
||||
if v is not None:
|
||||
total += v
|
||||
found_any = True
|
||||
return total if found_any else None
|
||||
|
||||
|
||||
def parse_bilan(entry):
|
||||
"""Retourne (annee, type_bilan, ca, resultat_net) ou None si non exploitable."""
|
||||
if entry.get("deleted"):
|
||||
return None
|
||||
if entry.get("confidentiality") not in (None, "Public"):
|
||||
return None
|
||||
bilan_saisi = entry.get("bilanSaisi")
|
||||
if not bilan_saisi:
|
||||
return None
|
||||
bilan = bilan_saisi.get("bilan", {})
|
||||
identite = bilan.get("identite", {})
|
||||
type_bilan = identite.get("codeTypeBilan")
|
||||
mapping = TYPE_BILAN_MAPPING.get(type_bilan)
|
||||
if mapping is None:
|
||||
return None
|
||||
date_cloture = identite.get("dateClotureExercice")
|
||||
if not date_cloture or len(date_cloture) < 4:
|
||||
return None
|
||||
annee = date_cloture[:4]
|
||||
pages = bilan.get("detail", {}).get("pages", [])
|
||||
ca = sum_codes(pages, mapping["ca"])
|
||||
rn = sum_codes(pages, mapping["rn"])
|
||||
if ca is None and rn is None:
|
||||
return None
|
||||
return annee, type_bilan, ca, rn
|
||||
|
||||
|
||||
MAX_PAGES_PAR_LOT = 100 # garde-fou : 100*PAGE_SIZE = 10 000 resultats max par lot,
|
||||
# au-dela quelque chose ne tourne pas rond (curseur bloque
|
||||
# ou volume anormal) -- ne jamais boucler indefiniment.
|
||||
|
||||
|
||||
def fetch_bilans_for_batch(sirens, auth):
|
||||
# Le curseur searchAfter doit porter sur l'identifiant unique du document
|
||||
# (champ "id", ex. un ObjectId), jamais sur le SIREN : un SIREN a en general
|
||||
# plusieurs bilans (plusieurs annees, parfois individuel + consolide), donc
|
||||
# il revient plusieurs fois dans les resultats -- l'utiliser comme curseur
|
||||
# provoquait un faux "curseur bloque" des qu'un SIREN se trouvait en
|
||||
# dernniere position d'une page, abandonnant le lot prematurement et
|
||||
# perdant les bilans non encore recuperes (bug trouve le 14/09/2026 :
|
||||
# ~14% des lots du departement 31 perdaient ainsi une partie de leur
|
||||
# historique). Verifie empiriquement : le champ "id" donne une pagination
|
||||
# propre, sans doublon ni repli, contrairement au SIREN.
|
||||
out = []
|
||||
search_after = None
|
||||
pages_lues = 0
|
||||
while True:
|
||||
pages_lues += 1
|
||||
if pages_lues > MAX_PAGES_PAR_LOT:
|
||||
log(f" !! lot bloque apres {MAX_PAGES_PAR_LOT} pages "
|
||||
f"(searchAfter={search_after}) -- abandon de ce lot pour eviter une "
|
||||
f"boucle infinie, {len(out)} resultats deja recuperes conserves")
|
||||
break
|
||||
params = [("siren[]", s) for s in sirens]
|
||||
params.append(("pageSize", str(PAGE_SIZE)))
|
||||
if search_after:
|
||||
params.append(("searchAfter", search_after))
|
||||
url = API_BILANS_SAISIS + "?" + urllib.parse.urlencode(params)
|
||||
data = http_get_json(url, auth)
|
||||
time.sleep(RATE_DELAY)
|
||||
if not data:
|
||||
break
|
||||
out.extend(data)
|
||||
if len(data) < PAGE_SIZE:
|
||||
break
|
||||
nouveau_search_after = data[-1].get("id")
|
||||
if not nouveau_search_after or nouveau_search_after == search_after:
|
||||
log(f" !! curseur searchAfter bloque sur {nouveau_search_after} -- "
|
||||
f"abandon de ce lot, {len(out)} resultats deja recuperes conserves")
|
||||
break
|
||||
search_after = nouveau_search_after
|
||||
return out
|
||||
|
||||
|
||||
def upsert_finance(conn, table, siren, annee, ca, rn):
|
||||
sql = (
|
||||
f"INSERT INTO `{table}` (ENT_Siren, ENT_FinancesAnnee, ENT_FinancesCa, ENT_FinancesResultatNet) "
|
||||
f"VALUES (%s,%s,%s,%s) "
|
||||
f"ON DUPLICATE KEY UPDATE ENT_FinancesCa=VALUES(ENT_FinancesCa), "
|
||||
f"ENT_FinancesResultatNet=VALUES(ENT_FinancesResultatNet)"
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, (siren, annee, ca, rn))
|
||||
|
||||
|
||||
def backfill_departement(conn, dep, auth):
|
||||
table1 = f"Entreprises{dep}Phase1"
|
||||
table3 = f"Entreprises{dep}Phase3"
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(f"SELECT ENT_Siren FROM `{table1}`")
|
||||
sirens = [row[0] for row in cur.fetchall()]
|
||||
|
||||
log(f"=== Departement {dep} : {len(sirens)} entreprises a traiter ===")
|
||||
total_rows = 0
|
||||
total_ignores_confidentiel = 0
|
||||
total_types_inconnus = 0
|
||||
unknown_types = {}
|
||||
|
||||
for i in range(0, len(sirens), BATCH_SIZE):
|
||||
batch = sirens[i:i + BATCH_SIZE]
|
||||
try:
|
||||
entries = fetch_bilans_for_batch(batch, auth)
|
||||
except QuotaExceededError:
|
||||
log(f" !! quota INPI depasse au lot {i}-{i+len(batch)} -- arret immediat du "
|
||||
f"departement {dep} (inutile de continuer, le quota est global au compte, "
|
||||
f"pas au departement)")
|
||||
raise
|
||||
except Exception as e:
|
||||
log(f" !! lot {i}-{i+len(batch)} : erreur {e}, on continue")
|
||||
continue
|
||||
|
||||
# best[(siren, annee)] = (priorite, ca, rn) -- on ne garde que le
|
||||
# bilan de plus haute priorite (cf TYPE_PRIORITY) par SIREN+annee,
|
||||
# pour ne jamais laisser un bilan consolide (K) ecraser au hasard
|
||||
# le bilan individuel (C) de la meme annee, ou l'inverse.
|
||||
best = {}
|
||||
for entry in entries:
|
||||
siren = entry.get("siren")
|
||||
if not siren:
|
||||
continue
|
||||
if entry.get("confidentiality") not in (None, "Public"):
|
||||
total_ignores_confidentiel += 1
|
||||
continue
|
||||
bilan_saisi = entry.get("bilanSaisi")
|
||||
if bilan_saisi:
|
||||
type_bilan = bilan_saisi.get("bilan", {}).get("identite", {}).get("codeTypeBilan")
|
||||
if type_bilan and type_bilan not in TYPE_BILAN_MAPPING:
|
||||
total_types_inconnus += 1
|
||||
unknown_types[type_bilan] = unknown_types.get(type_bilan, 0) + 1
|
||||
parsed = parse_bilan(entry)
|
||||
if parsed is None:
|
||||
continue
|
||||
annee, type_bilan, ca, rn = parsed
|
||||
prio = TYPE_PRIORITY.get(type_bilan, 99)
|
||||
key = (siren, annee)
|
||||
current = best.get(key)
|
||||
if current is None or prio < current[0]:
|
||||
best[key] = (prio, ca, rn)
|
||||
|
||||
for (siren, annee), (_prio, ca, rn) in best.items():
|
||||
upsert_finance(conn, table3, siren, annee, ca, rn)
|
||||
total_rows += 1
|
||||
|
||||
if (i // BATCH_SIZE) % 20 == 0:
|
||||
log(f" ... {i+len(batch)}/{len(sirens)} entreprises traitees, "
|
||||
f"{total_rows} lignes financieres upsertees jusqu'ici")
|
||||
|
||||
log(f"Departement {dep} termine : {total_rows} lignes financieres upsertees dans {table3} "
|
||||
f"({total_ignores_confidentiel} bilans confidentiels ignores, "
|
||||
f"{total_types_inconnus} types de bilan non mappes : {unknown_types})")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--departements", default=None,
|
||||
help="Liste separee par des virgules (defaut : tous ceux de ENT_Departements)")
|
||||
ap.add_argument("--env-file", default=os.path.join(os.path.dirname(__file__), ".env"))
|
||||
args = ap.parse_args()
|
||||
|
||||
env = load_env(args.env_file)
|
||||
|
||||
conn = pymysql.connect(
|
||||
host=env["ENTREPRISES_DB_HOST"], port=int(env.get("ENTREPRISES_DB_PORT", 3306)),
|
||||
user=env["ENTREPRISES_DB_USER"], password=env["ENTREPRISES_DB_PASS"],
|
||||
database=env["ENTREPRISES_DB_NAME"], charset="utf8mb4", autocommit=True,
|
||||
)
|
||||
|
||||
if args.departements:
|
||||
departements = [d.strip() for d in args.departements.split(",") if d.strip()]
|
||||
else:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT DEP_Code FROM ENT_Departements ORDER BY DEP_Code")
|
||||
departements = [row[0] for row in cur.fetchall()]
|
||||
|
||||
log(f"Departements a traiter : {departements}")
|
||||
auth = {
|
||||
"token": login(env["INPI_USERNAME"], env["INPI_PASSWORD"]),
|
||||
"username": env["INPI_USERNAME"],
|
||||
"password": env["INPI_PASSWORD"],
|
||||
}
|
||||
log("Connexion INPI reussie")
|
||||
|
||||
try:
|
||||
for dep in departements:
|
||||
backfill_departement(conn, dep, auth)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -115,6 +115,26 @@ NAF_CODES = [
|
||||
'98.20Z', '99.00Z',
|
||||
]
|
||||
|
||||
# Plage de divisions (2 premiers chiffres du code NAF) par section -- permet de
|
||||
# limiter le repli par code NAF precis (3e niveau) aux seuls codes de la section
|
||||
# en cours, au lieu de retester les 732 codes en entier a chaque section plafonnee
|
||||
# (bug corrige le 13/09/2026 : provoquait un blocage de plusieurs heures sur les
|
||||
# grandes villes, ex. Toulouse, ou plusieurs sections restent plafonnees).
|
||||
SECTION_DIVISIONS = {
|
||||
"A": range(1, 4), "B": range(5, 10), "C": range(10, 34), "D": range(35, 36),
|
||||
"E": range(36, 40), "F": range(41, 44), "G": range(45, 48), "H": range(49, 54),
|
||||
"I": range(55, 57), "J": range(58, 64), "K": range(64, 67), "L": range(68, 69),
|
||||
"M": range(69, 76), "N": range(77, 83), "O": range(84, 85), "P": range(85, 86),
|
||||
"Q": range(86, 89), "R": range(90, 94), "S": range(94, 97), "T": range(97, 99),
|
||||
"U": range(99, 100),
|
||||
}
|
||||
|
||||
|
||||
def codes_for_section(section):
|
||||
divisions = SECTION_DIVISIONS[section]
|
||||
return [c for c in NAF_CODES if int(c[:2]) in divisions]
|
||||
|
||||
|
||||
CAP = 10000
|
||||
PAGE_SIZE = 25
|
||||
RATE_DELAY = 0.15 # ~6-7 req/s, sous le quota documente de l'API
|
||||
@@ -128,6 +148,7 @@ CREATE TABLE IF NOT EXISTS `Entreprises{dep}Phase1` (
|
||||
`ENT_AdresseSiege` varchar(255) DEFAULT NULL,
|
||||
`ENT_CodePostal` varchar(20) DEFAULT NULL,
|
||||
`ENT_Commune` varchar(100) DEFAULT NULL,
|
||||
`ENT_CodeCommune` char(5) DEFAULT NULL,
|
||||
`ENT_Departement` char(3) DEFAULT NULL,
|
||||
`ENT_Region` char(2) DEFAULT NULL,
|
||||
`ENT_DateCreation` date DEFAULT NULL,
|
||||
@@ -154,8 +175,10 @@ CREATE TABLE IF NOT EXISTS `Entreprises{dep}Phase1` (
|
||||
`ENT_DateImport` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ENT_DateMajInsee` datetime DEFAULT NULL,
|
||||
`ENT_ReponseBruteJson` json DEFAULT NULL,
|
||||
PRIMARY KEY (`ENT_Siren`)
|
||||
PRIMARY KEY (`ENT_Siren`),
|
||||
KEY `idx_commune` (`ENT_CodeCommune`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8
|
||||
"""
|
||||
|
||||
DDL_PHASE2 = """
|
||||
@@ -183,7 +206,7 @@ CREATE TABLE IF NOT EXISTS `Entreprises{dep}Phase3` (
|
||||
DDL_COMMUNES = """
|
||||
CREATE TABLE IF NOT EXISTS `ENT_Communes` (
|
||||
`COM_CodeInsee` char(5) NOT NULL,
|
||||
`COM_CodePostal` varchar(20) DEFAULT NULL,
|
||||
`COM_CodePostal` varchar(100) DEFAULT NULL,
|
||||
`COM_Ville` varchar(100) NOT NULL,
|
||||
`COM_Departement` char(3) NOT NULL,
|
||||
`COM_Latitude` decimal(10,6) DEFAULT NULL,
|
||||
@@ -193,6 +216,25 @@ CREATE TABLE IF NOT EXISTS `ENT_Communes` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
DDL_DEPARTEMENTS = """
|
||||
CREATE TABLE IF NOT EXISTS `ENT_Departements` (
|
||||
`DEP_Code` char(3) NOT NULL,
|
||||
`DEP_Nom` varchar(100) NOT NULL,
|
||||
`DEP_NbEntreprises` int NOT NULL DEFAULT 0,
|
||||
`DEP_DateExtractionInitiale` datetime DEFAULT NULL,
|
||||
`DEP_DateDerniereSyncFormalites` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`DEP_Code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
DDL_CONFIG = """
|
||||
CREATE TABLE IF NOT EXISTS `ENT_Config` (
|
||||
`CFG_Cle` varchar(100) NOT NULL,
|
||||
`CFG_Valeur` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`CFG_Cle`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
@@ -209,11 +251,51 @@ def http_get_json(url, retries=5):
|
||||
time.sleep(1 + attempt)
|
||||
|
||||
|
||||
# Paris, Lyon et Marseille (PLM) : dans les donnees Sirene, le siege d'une
|
||||
# entreprise de ces trois villes est quasi toujours code au niveau de
|
||||
# l'arrondissement (75101-75120, 69381-69389, 13201-13216), jamais au code
|
||||
# commune englobant (75056, 69123, 13055) -- verifie empiriquement le 14
|
||||
# septembre 2026 sur Marseille : code_commune=13055 ne renvoie qu'une dizaine
|
||||
# de resultats, alors que chaque arrondissement (13201 a 13216) est a lui
|
||||
# seul plafonne ou proche du plafond de 10 000. Le code englobant est donc
|
||||
# remplace par la liste de ses arrondissements avant extraction.
|
||||
COMMUNES_ARRONDISSEMENTS = {"75": "75056", "69": "69123", "13": "13055"}
|
||||
|
||||
|
||||
def fetch_arrondissements_geo(dep):
|
||||
url = "https://geo.api.gouv.fr/communes?" + urllib.parse.urlencode(
|
||||
{"codeDepartement": dep, "type": "arrondissement-municipal",
|
||||
"fields": "code,nom,centre,codesPostaux,departement"}
|
||||
)
|
||||
return http_get_json(url)
|
||||
|
||||
|
||||
def fetch_communes_geo(dep):
|
||||
url = API_GEO.format(dep=dep) + "?" + urllib.parse.urlencode(
|
||||
{"fields": "code,nom,centre,codesPostaux,departement"}
|
||||
)
|
||||
return http_get_json(url)
|
||||
communes = http_get_json(url)
|
||||
code_englobant = COMMUNES_ARRONDISSEMENTS.get(dep)
|
||||
if code_englobant:
|
||||
communes = [c for c in communes if c["code"] != code_englobant]
|
||||
communes.extend(fetch_arrondissements_geo(dep))
|
||||
return communes
|
||||
|
||||
|
||||
def fetch_departement_nom(dep):
|
||||
url = f"https://geo.api.gouv.fr/departements/{dep}?fields=code,nom"
|
||||
data = http_get_json(url)
|
||||
return data.get("nom", dep)
|
||||
|
||||
|
||||
def upsert_departement(conn, dep, nom, nb_entreprises):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO ENT_Departements (DEP_Code, DEP_Nom, DEP_NbEntreprises, DEP_DateExtractionInitiale) "
|
||||
"VALUES (%s, %s, %s, NOW()) "
|
||||
"ON DUPLICATE KEY UPDATE DEP_Nom=VALUES(DEP_Nom), DEP_NbEntreprises=VALUES(DEP_NbEntreprises)",
|
||||
(dep, nom, nb_entreprises),
|
||||
)
|
||||
|
||||
|
||||
NON_DIFFUSIBLE = "[NON-DIFFUSIBLE]"
|
||||
@@ -254,6 +336,7 @@ def map_entreprise(r):
|
||||
"ENT_AdresseSiege": clean(siege.get("adresse")),
|
||||
"ENT_CodePostal": clean(siege.get("code_postal")),
|
||||
"ENT_Commune": clean(siege.get("libelle_commune")),
|
||||
"ENT_CodeCommune": clean(siege.get("commune")),
|
||||
"ENT_Departement": clean(siege.get("departement")),
|
||||
"ENT_Region": clean(siege.get("region")),
|
||||
"ENT_DateCreation": clean(r.get("date_creation")),
|
||||
@@ -329,7 +412,7 @@ def fetch_commune_entreprises(code_commune, seen):
|
||||
# au filtre de section.
|
||||
log(f" commune {code_commune} section {section} encore plafonnee ({sec_total}), "
|
||||
f"repli par code NAF precis")
|
||||
for code in NAF_CODES:
|
||||
for code in codes_for_section(section):
|
||||
code_params = dict(base, activite_principale=code)
|
||||
code_total = probe_total(code_params)
|
||||
if code_total == 0:
|
||||
@@ -383,6 +466,8 @@ def ensure_tables(conn, dep):
|
||||
cur.execute(DDL_PHASE2.format(dep=dep))
|
||||
cur.execute(DDL_PHASE3.format(dep=dep))
|
||||
cur.execute(DDL_COMMUNES)
|
||||
cur.execute(DDL_DEPARTEMENTS)
|
||||
cur.execute(DDL_CONFIG)
|
||||
|
||||
|
||||
def ensure_communes(conn, dep, communes_geo):
|
||||
@@ -431,8 +516,16 @@ def run_department(conn, dep):
|
||||
total_finances += 1
|
||||
if results:
|
||||
log(f" {c['nom']} ({c['code']}) : {len(results)} entreprises")
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(f"SELECT COUNT(*) FROM `{table1}`")
|
||||
(nb_total,) = cur.fetchone()
|
||||
nom_dep = fetch_departement_nom(dep)
|
||||
upsert_departement(conn, dep, nom_dep, nb_total)
|
||||
|
||||
log(f"Departement {dep} termine : {total_upserts} entreprises upsertees dans {table1}, "
|
||||
f"{total_finances} lignes financieres upsertees dans {table3}")
|
||||
f"{total_finances} lignes financieres upsertees dans {table3} (total {nb_total} -- "
|
||||
f"ENT_Departements mise a jour)")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
* capacité à détecter un déploiement resté en retard.
|
||||
*/
|
||||
|
||||
const ASPIC_VERSION = 'V1.23.25';
|
||||
const ASPIC_VERSION = 'V1.23.28';
|
||||
|
||||
if (basename($_SERVER['SCRIPT_NAME']) === 'version.php') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
Reference in New Issue
Block a user