Moteur de traitement données énergétiques françaises - Architecture Polars/DuckDB pour flux Enedis
Project description
⚡ ElectriCore - Moteur de traitement données énergétiques
ElectriCore est un outil libre pour reprendre le contrôle des données du réseau électrique français. Architecture moderne Polars + DuckDB pour transformer les flux bruts Enedis en données exploitables par LibreWatt, Odoo et autres outils de suivi énergétique.
🎯 Objectifs
Un outil de calcul énergétique performant et maintenable pour :
- ✅ Transformer les flux XML/CSV Enedis en données structurées
- ✅ Calculer les indicateurs essentiels (périmètre, abonnements, consommations, TURPE)
- ✅ Exposer les données via API REST sécurisée
- ✅ Intégrer avec Odoo et autres systèmes tiers
🏗️ Architecture - Modules
electricore/
├── ingestion/ # 📥 INGESTION - Flux Enedis → DuckDB (ELT dlt + dbt)
│
├── core/ # 🧮 CORE - Calculs énergétiques ERP-agnostiques (Polars)
│ ├── models/ # Modèles Pandera (validation des données)
│ ├── pipelines/ # Pipelines de calcul (historique, abonnements, énergie, turpe, accise…)
│ ├── builds/ # Livrables assemblés (contexte mensuel, rapports — ADR-0019)
│ └── loaders/ # Query builders (DuckDB, Parquet)
│
├── integrations/ # 🔌 INTEGRATIONS - Adaptateurs ERP (cf. ADR-0016)
│ └── odoo/ # OdooReader, OdooQuery, OdooWriter, helpers, schémas Pandera Odoo
│
├── api/ # 🌐 API - Accès aux données (FastAPI)
│
└── bot/ # 🤖 BOT Telegram - UI opérationnelle, client de l'API
core/ ne dépend ni d'Odoo ni d'aucun ERP — règle exécutable via le test tests/architecture/test_core_purity.py (cf. ADR-0016). Les orchestrations qui composent Enedis et Odoo (rapprochement facturation, accise/CTA) vivent dans integrations/odoo/.
Diagramme de flux
graph TB
SFTP_Enedis[/SFTP Enedis\] --> Ingestion_Enedis[Ingestion<br/>#40;dlt + dbt#41;]
SFTP_Axpo[/SFTP Axpo<br/>Courbes\] -.-> Ingestion_Axpo[Ingestion<br/>#40;dlt + dbt#41;]
Ingestion_Enedis --> DuckDB[(DuckDB)]
Ingestion_Axpo -.-> DuckDB
Odoo[(Odoo ERP)] --> OdooReader[OdooReader]
OdooWriter[OdooWriter] --> Odoo
DuckDB --> API[API REST<br/>#40;FastAPI#41;]
DuckDB -->|Query Builder| Core[Core Pipelines<br/>#40;Polars#41;]
OdooReader -->|Query Builder| Core
OdooReader --> API
Core --> API
Core --> OdooWriter
API -->|JSON| Client[\Clients API/]
style API fill:#4CAF50,stroke:#2E7D32,stroke-width:3px,color:#fff
style DuckDB fill:#1976D2,stroke:#0D47A1,color:#fff
style Odoo fill:#FF9800,stroke:#E65100,color:#fff
style Core fill:#9C27B0,stroke:#4A148C,color:#fff
style Ingestion_Axpo stroke-dasharray: 5 5
📥 Module ingestion — Flux Enedis → DuckDB
Ingestion modulaire basée sur dlt (landing brut) et dbt (linéarisation SQL) pour les flux Enedis.
Flux supportés
| Flux | Description | Tables générées |
|---|---|---|
| C15 | Changements contractuels | flux_c15 |
| F12 | Facturation distributeur | flux_f12 |
| F15 | Facturation détaillée | flux_f15_detail |
| R15 | Relevés avec événements | flux_r15, flux_r15_acc |
| R151 | Relevés périodiques | flux_r151 |
| R64 | Relevés JSON timeseries | flux_r64 |
Architecture modulaire
# Ingestion avec transformers chaînables
encrypted_files | decrypt_transformer | unzip_transformer | parse_transformer
Transformers disponibles :
crypto.py- Déchiffrement AESarchive.py- Extraction ZIPparsers.py- Parsing XML/CSV
Utilisation
# Test rapide (2 fichiers)
uv run python -m electricore.ingestion test
# R151 complet (~6 secondes)
uv run python -m electricore.ingestion r151
# Tous les flux (production)
uv run python -m electricore.ingestion all
Résultat : Base DuckDB electricore/ingestion/flux_enedis_pipeline.duckdb avec toutes les tables flux.
📖 Documentation complète : electricore/ingestion/README.md
🧮 Module Core - Calculs Énergétiques Polars
Pipelines de calculs énergétiques basés sur Polars pur (LazyFrames + expressions fonctionnelles).
Pipelines disponibles
1. Historique - Événements contractuels enrichis (ex-périmètre, ADR-0013)
from electricore.core.pipelines.historique import pipeline_historique
from electricore.core.loaders import c15
# Depuis DuckDB avec Query Builder
historique_lf = (
c15()
.filter({"Date_Evenement": ">= '2024-01-01'"})
.lazy()
)
historique_enrichi = pipeline_historique(historique_lf)
# Événements C15 enrichis : impacte_abonnement, impacte_energie, resume_modification, ...
2. Abonnements - Périodes d'abonnement
from electricore.core.pipelines.abonnements import pipeline_abonnements
# Périodes homogènes de la part fixe, dérivées de l'historique enrichi
abonnements_lf = pipeline_abonnements(historique_enrichi)
# Colonnes: pdl, mois_annee, nb_jours, puissance_souscrite_kva, formule_tarifaire_acheminement, ...
3. Énergies - Consommations par cadran
from electricore.core.pipelines.energie import pipeline_energie
from electricore.core.loaders import releves_harmonises
releves_lf = releves_harmonises().lazy()
energie_lf = pipeline_energie(historique_enrichi, releves_lf)
# Colonnes: pdl, debut, fin, energie_base_kwh, energie_hp_kwh, energie_hc_kwh, ...
4. TURPE - Tarif d'acheminement réseau
from electricore.core.pipelines.turpe import ajouter_turpe_fixe, ajouter_turpe_variable
# TURPE fixe (périodes d'abonnement)
abonnements_turpe = ajouter_turpe_fixe(abonnements_lf)
# Colonnes: ..., turpe_fixe_eur
# TURPE variable (périodes d'énergie)
energie_turpe = ajouter_turpe_variable(energie_lf)
# Colonnes: ..., turpe_variable_eur
5. Contexte mensuel - Pipeline complet (ADR-0019)
from electricore.core.builds.contexte_mensuel import contexte_du_mois
# Compose : historique → abonnements + énergie → facturation mensuelle
ctx = contexte_du_mois("2026-05-01") # None → dernier mois disponible
ctx.facturation_mensuelle # DataFrame agrégé du mois (validé Pandera)
ctx.abonnements # LazyFrame des périodes d'abonnement
ctx.energie # LazyFrame des périodes d'énergie
ctx.historique_enrichi # LazyFrame de l'historique enrichi
🔧 Interfaces de Requêtage
DuckDB Query Builder - Architecture Fonctionnelle Modulaire
Architecture en 6 modules pour performance et maintenabilité :
config.py- Configuration et connexions DuckDBexpressions.py- Expressions Polars pures réutilisablestransforms.py- Transformations composables aveccompose()sql.py- Génération SQL fonctionnelle (dataclasses frozen)query.py- Query builder immutable (DuckDBQuery)__init__.py- API publique + helper_CTEQuerypour requêtes CTE
from electricore.core.loaders import c15, r151, releves, releves_harmonises
# Historique périmètre (flux C15)
historique = (
c15()
.filter({"Date_Evenement": ">= '2024-01-01'"})
.limit(100)
.collect()
)
# Relevés périodiques (flux R151)
relevés = (
r151()
.filter({"pdl": ["PDL123", "PDL456"]})
.limit(1000)
.lazy() # Retourne LazyFrame pour optimisations
)
# Relevés unifiés (R151 + R15) avec CTE
tous_releves = releves().collect()
# Relevés harmonisés (R151 + R64) avec CTE
releves_cross_flux = (
releves_harmonises()
.filter({"flux_origine": "R64"})
.collect()
)
Fonctions disponibles : c15(), r151(), r15(), f15(), r64(), releves(), releves_harmonises()
Caractéristiques :
- ✅ Immutabilité garantie (frozen dataclasses)
- ✅ Composition fonctionnelle pure
- ✅ Lazy evaluation optimisée
- ✅ Support CTE (Common Table Expressions)
- ✅ Validation Pandera intégrée
📖 Documentation : docstrings des modules electricore/core/loaders/duckdb/
Odoo Query Builder - Intégration ERP
from electricore.integrations.odoo import OdooReader
import polars as pl
config = {
'url': 'https://odoo.example.com',
'db': 'production',
'username': 'api_user',
'password': 'secret'
}
with OdooReader(config) as odoo:
# Query builder avec navigation relationnelle
factures_df = (
odoo.query('sale.order', domain=[('x_pdl', '!=', False)])
.follow('invoice_ids', fields=['name', 'invoice_date', 'amount_total'])
.filter(pl.col('amount_total') > 100)
.collect()
)
# Enrichissement avec données liées
commandes_enrichies = (
odoo.query('sale.order', fields=['name', 'date_order'])
.enrich('partner_id', fields=['name', 'email'])
.collect()
)
Méthodes disponibles : .query(), .follow(), .enrich(), .filter(), .select(), .rename(), .collect()
📖 Documentation complète : docs/odoo-query-builder.md
🌐 Module API - Accès aux Données
API REST sécurisée basée sur FastAPI pour accéder aux données flux depuis DuckDB.
Endpoints
Publics (sans authentification)
GET /- Informations API et tables disponiblesGET /health- Statut API et base de donnéesGET /docs- Documentation Swagger interactive
Sécurisés (authentification requise)
GET /flux/{table_name}- Données d'une table flux (+/info: métadonnées, fraîcheur)POST /ingestion/run,GET /ingestion/jobs- Déclenchement et suivi des jobs d'ingestionGET /taxes/...- Accise, CTA (exports Arrow/XLSX)GET /facturation/...- Documents mensuels, check pré-facturation Odoo (XLSX)GET /admin/api-keys- Configuration clés API
Utilisation
# Démarrer l'API
uv run uvicorn electricore.api.main:app --reload
# Requête avec authentification
curl -H "X-API-Key: votre_cle" "http://localhost:8000/flux/r151?limit=10"
# Filtrer par PDL
curl -H "X-API-Key: votre_cle" "http://localhost:8000/flux/c15?prm=12345678901234"
# Métadonnées d'une table
curl -H "X-API-Key: votre_cle" "http://localhost:8000/flux/r151/info"
📖 Documentation complète : electricore/api/README.md
🚀 Installation & Usage
Prérequis
- Python 3.12+
- uv
Installation
# Cloner le projet
git clone https://github.com/Energie-De-Nantes/electricore.git
cd electricore
# Installation runtime (core + API + bot)
uv sync
# + ingestion SFTP Enedis, dlt + dbt (pour serveur de collecte)
uv sync --extra ingestion --extra dbt
# + libs notebooks (marimo, altair, plotly, vegafusion, vl-convert)
uv sync --extra viz
# Tout (dev local complet)
uv sync --extra ingestion --extra dbt --extra viz
Configuration initiale
Créer un fichier .env à la racine du projet et renseigner les variables selon les modules utilisés :
# === API — obligatoire pour démarrer l'API ===
# Générer : python -c "import secrets; print(secrets.token_urlsafe(32))"
API_KEY=votre_cle_api_secrete
# Ou plusieurs clés : API_KEYS=cle1,cle2,cle3
# === BASE DE DONNÉES ===
# Chemin vers le fichier DuckDB produit par l'ingestion
# Par défaut : electricore/ingestion/flux_enedis_pipeline.duckdb (relatif au cwd)
# En production, utiliser un chemin absolu :
# DUCKDB_PATH=/srv/<slug>/data/flux_enedis_pipeline.duckdb
# === INGESTION ENEDIS — obligatoire pour l'ingestion ===
SFTP__URL=sftp://utilisateur:mot_de_passe@hote:22/chemin
# Trousseau de clés AES (ADR-0037) : un <label> parlant par clé, sélection par essai.
# Garder les anciennes clés dans le trousseau préserve l'accès aux archives passées.
# __IV optionnel (ADR-0040) : absent ⇒ schéma IV-préfixé (AES-256) ; présent ⇒ IV-fixe (AES-128).
AES__TROUSSEAU__aes256_2026__KEY=cle_hex_fournie_par_enedis # AES-256 : pas de __IV
# AES__TROUSSEAU__aes128_2024__KEY=ancienne_cle_hex # AES-128 : clé + IV
# AES__TROUSSEAU__aes128_2024__IV=ancien_iv_hex
# === ODOO — pour les calculs CTA/Accise et la réconciliation ===
ODOO_ENV=test # ou prod
ODOO_TEST_URL=https://votre-instance.odoo.com
ODOO_TEST_DB=nom_de_la_base
ODOO_TEST_USERNAME=utilisateur@example.com
ODOO_TEST_PASSWORD=mot_de_passe
# ODOO_PROD_URL / ODOO_PROD_DB / ODOO_PROD_USERNAME / ODOO_PROD_PASSWORD
# === BOT TELEGRAM — optionnel ===
TELEGRAM_BOT_TOKEN=token_obtenu_via_botfather # convention : @<slug>_electricore_bot
TELEGRAM_ALLOWED_USERS=123456789 # IDs séparés par virgule
TELEGRAM_NOTIFY_CHAT_ID= # chat des alertes (échec d'ingestion) ; vide = désactivé
Commandes essentielles
# Tests
uv run --group test pytest -q
# Ingestion complète (nécessite --extra ingestion)
uv run python -m electricore.ingestion all
# API FastAPI
uv run uvicorn electricore.api.main:app --reload
# Notebooks interactifs (Marimo)
uv run marimo edit notebooks/
Déploiement VPS (Docker)
Pour exécuter ElectriCore en production sur un VPS — ingestion planifiée, API + bot 24/7, TLS automatique — un script d'installation provisionne tout depuis un VPS Ubuntu/Debian frais :
ssh root@<vps>
curl -fsSL https://raw.githubusercontent.com/Energie-De-Nantes/electricore/main/deploy/install.sh -o install.sh
EDITOR=nano sudo -E bash install.sh \
--slug <slug> --domain <slug>.electricore.fr --email ops@example.com
Le script crée un user système dédié, installe Docker, configure UFW, télécharge la stack, ouvre .env dans l'éditeur, vérifie le DNS, démarre les conteneurs et lance une ingestion test (~5-10 min total).
Guide complet : docs/deploiement.md (Quickstart, multi-instance, mode SFTP distant vs collocés, rotation clés AES, sauvegarde, mise à jour, migration depuis l'ancien layout, dépannage).
🧪 Tests & Validation
Suite de tests avec 671 tests passants (35 skips légitimes, juin 2026 ✅) :
Infrastructure de test
- ✅ Configuration pytest : 8 markers (unit, integration, slow, smoke, duckdb, odoo, hypothesis, skip_ci)
- ✅ Fixtures partagées : Connexions DuckDB temporaires, données minimales, helpers d'assertion
- ✅ Tests paramétrés :
@pytest.mark.parametrizepour réduire la duplication - ✅ Tests snapshot : Syrupy pour détection automatique de régression
- ✅ Golden d'ingestion : fixtures générées depuis les XSD Enedis, parité dbt garantie en CI
- ✅ Script anonymisation : Extraction sécurisée de cas métier réels
Types de tests
- Tests unitaires - Expressions Polars pures (historique, abonnements, énergie, TURPE, taxes), bot, sérialiseurs API
- Tests d'intégration - Endpoints API (flux, taxes, facturation, ingestion), snapshots de pipelines
- Tests d'ingestion - Golden dbt par flux, crypto AES, xml→dict, modes du runner
- Tests d'architecture - Pureté ERP de
core/(ADR-0016), imports par rôle (ADR-0019), topologies bot/builds - Fixtures métier - Cas réels (MCT, MES/RES, changements)
Commandes
# Tous les tests
uv run --group test pytest -q
# Tests rapides uniquement
uv run --group test pytest -m unit
# Exécution parallèle
uv run --group test pytest -n auto
# Avec coverage
uv run --group test pytest --cov=electricore --cov-report=html
Couverture : ~77% (juin 2026, seuil CI à 45%)
📖 Documentation complète : tests/README.md
🗺️ Roadmap
Complété ✅
- Migration Polars complète (périmètre, abonnements, énergies, turpe, accise/CTA)
- Query Builder DuckDB avec architecture fonctionnelle modulaire (6 modules)
- Connecteur Odoo avec Query Builder ;
core/ERP-agnostique (ADR-0016) - API FastAPI sécurisée avec authentification (hub central, ADR-0009)
- Ingestion ELT dlt + dbt en production (ADR-0020, ADR-0021)
- Bot Telegram par domaines métier (ADR-0022)
- Déploiement VPS Docker + script d'installation (ADR-0011)
- CI/CD GitHub Actions (tests + release d'images ghcr.io)
En cours 🔄
- Property-based testing des pipelines (issues #194–#198)
- Millésime et péremption des taux régulés (issues #185–#187)
- Centralisation de la configuration runtime (ADR-0025, issue #141)
À venir 📅
- Régularisation des contrats lissés (épique #191)
- API SOAP Enedis (alternative SFTP)
- Nouveaux connecteurs (Axpo, autres sources)
- Suivi des souscriptions aux services de données
📚 Documentation Complémentaire
- Ingestion README - Ingestion des flux Enedis
- API README - API REST et authentification
- Bot README - Bot Telegram : surface de commandes, alertes, no-ERP
- Query Builder DuckDB - Modules
query.py,sql.py,expressions.py - Query Builder Odoo - Intégration Odoo
- Conventions Dates - Formats temporels Enedis
- CONTEXT-MAP.md — Carte des contextes multi-modules (vocabulaire métier dans
electricore/core/CONTEXT.md, plus contextes ingestion/api/bot) - docs/adr/ — Décisions architecturales (monorepo, Polars, DuckDB, harmonisation R151…)
🤝 Contribution
Les contributions sont les bienvenues ! Avant toute modification :
- Lancer les tests :
uv run --group test pytest -q - Vérifier la cohérence avec les patterns Polars existants
- Documenter les nouvelles fonctionnalités
- Suivre les conventions de code du projet
📄 Licence
AGPL-3.0 - Voir LICENSE
🙏 Remerciements
- Polars - Framework data processing moderne
- DuckDB - Base analytique embarquée
- dlt - Chargement de données déclaratif
- FastAPI - Framework API performant
- Pandera - Validation schémas données
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file electricore-3.4.0rc2.tar.gz.
File metadata
- Download URL: electricore-3.4.0rc2.tar.gz
- Upload date:
- Size: 4.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
277f4ea5c47c9232eddf1dc820ffd42daf8a3340a31583618086a8a4a9244db5
|
|
| MD5 |
b75d2ec9831e9540eff0be0e52012a95
|
|
| BLAKE2b-256 |
088698d9491445423a368250a01d80ee411daed7460ef87e8735b23ae3f314ad
|
Provenance
The following attestation bundles were made for electricore-3.4.0rc2.tar.gz:
Publisher:
release.yml on Energie-De-Nantes/electricore
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
electricore-3.4.0rc2.tar.gz -
Subject digest:
277f4ea5c47c9232eddf1dc820ffd42daf8a3340a31583618086a8a4a9244db5 - Sigstore transparency entry: 1899695496
- Sigstore integration time:
-
Permalink:
Energie-De-Nantes/electricore@4c775f8959da3043f21f62a39f262ec718c235e9 -
Branch / Tag:
refs/tags/v3.4.0rc2 - Owner: https://github.com/Energie-De-Nantes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c775f8959da3043f21f62a39f262ec718c235e9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file electricore-3.4.0rc2-py3-none-any.whl.
File metadata
- Download URL: electricore-3.4.0rc2-py3-none-any.whl
- Upload date:
- Size: 308.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2308982696ff66ce2cf2338962895a8a4602113f79d3b81c3fb15d390b9984a1
|
|
| MD5 |
ead073fd9c47816d2830eef0fba19388
|
|
| BLAKE2b-256 |
65126ee7b18f16ef0f21b08b2c2e9655c687cfe6f5c085e817e17248be5179c7
|
Provenance
The following attestation bundles were made for electricore-3.4.0rc2-py3-none-any.whl:
Publisher:
release.yml on Energie-De-Nantes/electricore
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
electricore-3.4.0rc2-py3-none-any.whl -
Subject digest:
2308982696ff66ce2cf2338962895a8a4602113f79d3b81c3fb15d390b9984a1 - Sigstore transparency entry: 1899695916
- Sigstore integration time:
-
Permalink:
Energie-De-Nantes/electricore@4c775f8959da3043f21f62a39f262ec718c235e9 -
Branch / Tag:
refs/tags/v3.4.0rc2 - Owner: https://github.com/Energie-De-Nantes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c775f8959da3043f21f62a39f262ec718c235e9 -
Trigger Event:
push
-
Statement type: