---
tags: note
type: "note"
nature: info             # Options : portrait, lieu, info, concept, script, process, tuto, none
ressource: false
univers: PRO
projects:
  - "PROJET"
date: "2026-08-31"
priority: 10
---
# Import des données Budget dans Grist

Ceci est le résultat d’un échange avec Mistral : à nettoyer et valider
Privilégier la méthode Python qui semble la plus efficace et simple ici.

## Analyse initiale

### Besoin

L'objectif est multiple:
* Importer des données à partir de fichiers CSV ou Excel issus de GEF
* Traier et modifier ces données (ex. : renommage des colonnes, cacul d'identifiant, jointure entre tables, etc.
* Gérer des tables complémentaires pour catégoriser les données (ex. : classifier chaque marché selon les axes de travail DGE)


### Approches envisagées:

Les approches suivantes ont été étudiées:
1. Import direct des fichiers CSV et gestion des données dans Grist via des formules de caculs et des colonnes complémentaires
2. Automatisation avec Node-Red de chaque étape (lecture des données, traitement et import via l'API de Grist)
3. Utilisation d'une IA connectée à l'API de Grist via un MCP
4. Mise en place de scripts Python pour gérer l'ensemble de la chaïne de traitement

### Analyse des différents approches

#### 1. Import direct dans Grist

Cette approche manuelle n'est pas retenue car trop pénible à mettre en oeuvre et pas suffisamment automatisée

#### 2. Utilisation de Node-Red

Cette approche est simple et visuelle. Elles est adaptée pour des besoins basiques à des cas simples avec peu de tables.
Elle peut être aussi recommandée quand plusieurs outils spécifiques et complexes sont à interconnecter. Ce n'est pas le cas ici.

Elle présente plusieurs avantages mais n'a pas été retenu car :
* Elle n'apporte pas de vraie plus value par rapport à l'usage de script Python si ce n'est la construction graphique du flux
* Elle peut devenir complexe à gérer si de nombreuses tables et traitements sont à réaliser

Une analyse plus fine des nodes disponibles serait à réaliser, mais il semble préférable d'écarter Node-Red car il est fort probable que une grande partie des traitements soit à coder entièrement en Node JS. Dans ce cas, l'usage de scripts Python bien strucutrés semble tout aussi intéressant.

#### 3. Utilsiation d'une IA et du MCP Grist

A ce stade, cette approche semble relativement complexe pour construire le système. Elle nécessite de décrire l'ensemble des actions à réaliser.
L'apport de l'IA et du MCP est jugé faible dans le cas présent, l'ensemble des process étant stables et automatisables via des scripts.
Cette approche est idéale si les règles changent souvent ou sont trop complexes pour être codées manuellement ce qui n'est pas le cas ici.
Par contre il peut être intéressant de mobiliser l'IA pour produire un cadre de développement et la base des scripts à déployer.


#### 4. Utilisation de scripts Python

C'est la solution retenue ici. Elle apporte le meilleure compromis entre Node-Red qui nécessite de développer une part importane du code en Node JS et l'utilisation de l'IA et du MCP.
L'iA pourra être mobilisée pour construire el socle et l'enrichir progressivement.

**Avantage** : Très flexible, peut gérer des règles de mapping complexes.
**Inconvénient** : Nécessite d'écrire du code.

#### Comparatif des méthodes d'automatisation

| **Critère** | **API Grist (directe)** | **Node-RED** | **OpenCode + MCP Grist** |
| --- | --- | --- | --- |
| **Complexité** | Moyenne (nécessite de coder) | Faible (interface visuelle) | Élevée (configuration IA + MCP) |
| **Flexibilité** | Très élevée (contrôle total) | Moyenne (limité par les nœuds disponibles) | Élevée (si l'IA comprend les règles) |
| **Mapping de colonnes** | Manuel (dans le code) | Visuel (nœuds Function/Transform) | Automatisé (via IA) |
| **Gestion des erreurs** | Bonne (retours API détaillés) | Moyenne (dépend des logs Node-RED) | Variable (dépend de l'IA) |
| **Intégration continue** | Oui (scripts planifiés) | Oui (flux Node-RED déclenchés par événements) | Oui (si intégré à un workflow) |
| **Prérequis techniques** | Connaissance de l'API REST + JavaScript/Python | Connaissance de Node-RED | Connaissance d'OpenCode + MCP + IA |
| **Performance** | Rapide (requêtes HTTP directes) | Moyenne (dépend du flux) | Variable (dépend de l'IA) |
| **Maintenabilité** | Bonne (code versionnable) | Moyenne (flux à documenter) | Complexe (dépend de la stabilité de l'IA) |
| **Coût** | Gratuit (sauf quota API Grist) | Gratuit | Coût potentiel (OpenCode + MCP) |
| **Cas d'usage idéal** | Automatisation complète, scripts personnalisés | Pipelines ETL simples, intégration IoT | Automatisation intelligente, règles complexes |



### Analyse détaillée de la méthode Python

Python est le meilleur choix dans le cas présent. En effet il apporte à la fois :

1. **Évolutivité** :
    * Avec **10-15 tables**, un script Python sera **plus facile à maintenir** qu'un flux Node-RED complexe.
    * Vous pouvez **modulariser** votre code (ex: un fichier par table, des fonctions réutilisables pour les calculs).

2. **Flexibilité** :
    * Python permet de gérer **tous types de calculs** (même complexes) avec des bibliothèques comme `pandas`, `numpy`, ou des requêtes SQL locales avec `sqlite3`.
    * Exemple : pour faire des **jointures** entre vos CSV avant l'import, `pandas` le permet en une ligne : `df_merged = pd.merge(df_produits, df_categories, on="categorie_id")`
        
3. **Automatisation** :
    * Il est possible de **planifier** l'exécution de vos scripts avec `cron` (Linux/macOS) ou **Task Scheduler** (Windows).
    * Exemple : commande `cron` pour exécuter un script tous les jours à 2h : `0 2 * * * /usr/bin/python3 /chemin/vers/votre_script.py`
        
4. **Gestion des erreurs** :
    * Python offre un **contrôle fin** sur les erreurs (ex: gestion des lignes CSV manquantes, validation des données).
    * Exemple :

```python
try:
    df["prix_ttc"] = df["prix_ht"] * (1 + df["tva"] / 100)
except KeyError as e:
    print(f"Colonne manquante : {e}")
    # Envoyer une alerte ou corriger automatiquement
```

5. **Intégration avec Grist** :
    * L'API Grist est **facile à utiliser** avec la bibliothèque `requests` en Python.
    * Vous pouvez **créer les tables dynamiquement** si elles n'existent pas encore.


### Processus global

1. Définir la structure des tables au format JSON
2. Créer les tables dans Grist via l'API
3. Lire les fichiers CSV/XLSX
4. Exécuter les traitements nécessaire et appliquer les règles de mapping (une fonction par traitement unitaire - renommage de colone sur la base d'une correspondance ancienne/nouvelle colonne, extraction de données, calcul d'identifiants, etc.)
5. Importer les donénes dans Grist via l'API (focntion adaptée à prévoir)
  


## Implémentation

### Points d'attention

**Clés primaires/étrangères** :
    
* Grist gère les clés primaires via l'option `isPrimaryKey` dans l'API.
* Pour les clés étrangères, utilisez le type `Reference` et spécifiez la table cible.
* Exemple pour une colonne `user_id` (clé étrangère vers `users.id`) :
        
```json
{
  "name": "user_id",
  "type": "Reference",
  "referencedTable": "users"
}
```
        
**Types de données** :
    
* Grist a des types limités. Voici la correspondance avec SQL :
    
| SQL (SQLite) | Grist |
| --- | --- |
| `INTEGER` | `Integer` |
| `REAL` | `Number` |
| `TEXT` | `Text` |
| `DATE`/`DATETIME` | `Date` |
| `BOOLEAN` | `Boolean` |
| `BLOB` | `Text` (base64) |
        
**Limites de l'API Grist** :
    
* Rate limiting : 60 requêtes/minute (vérifiez [la documentation](https://support.getgrist.com/api/)).
* Taille maximale des données : 10 Mo par requête pour l'import.




---
CODE UTILE

* Utilisez `pandas` (Python) ou `csv-parse` (Node.js) pour lire le CSV, renommer les colonnes, et sauvegarder un nouveau fichier.
* Exemple en Python :
                
```python
import pandas as pd
df = pd.read_csv("input.csv")
df.rename(columns={"Ref_Routes": "routes_id"}, inplace=True)
df.to_csv("output_grist.csv", index=False)
```


Etant donné le cas d'usage présent (schéma stable, moyennement complexe avec de nombreux traitements sur les colonnes) la méthode qui semble recommandée ici est celle basée sur l'API Grist et des scripts Python ou Node.js.
Cela apporte un contrôle total et reproductible.


Exemple de prompt (à adapter)
    
> "J'ai un schéma SQL avec des tables `users` et `routes`. Dans mon CSV, les colonnes `Ref_Routes` et `Ref_Users` doivent être mappées respectivement à `routes_id` et `users_id` dans Grist. Génère un script Python qui utilise l'API Grist pour créer les tables et importer les données en appliquant ces règles."
    
2. **L'IA génère un script** comme :
    
```python
import requests
import pandas as pd

# Configuration
API_KEY = "VOTRE_CLE_API"
DOC_ID = "VOTRE_DOC_ID"
CSV_PATH = "data.csv"

# Lire le CSV
df = pd.read_csv(CSV_PATH)
df.rename(columns={
	"Ref_Routes": "routes_id",
	"Ref_Users": "users_id"
}, inplace=True)

# Créer les tables via l'API Grist
tables = [
	{"name": "routes", "columns": [{"name": "routes_id", "type": "Integer"}]},
	{"name": "users", "columns": [{"name": "users_id", "type": "Integer"}]}
]
for table in tables:
	requests.post(
		f"https://api.grist.com/api/docs/{DOC_ID}/tables",
		json=table,
		headers={"Authorization": f"Bearer {API_KEY}"}
	)

# Importer les données
requests.post(
	f"https://api.grist.com/api/docs/{DOC_ID}/tables/routes/rows",
	json={"records": df.to_dict("records")},
	headers={"Authorization": f"Bearer {API_KEY}"}
)
```
    

## **
---

## **8. Résumé des étapes clés pour votre cas**

1. **Préparez vos fichiers** :
    
    * Si possible, renommez les colonnes des CSV pour qu'elles correspondent au schéma Grist (via Node-RED, Python, ou OpenCode).
2. **Créez les tables dans Grist** :
    
    * **Automatiquement** via l'API (pour la production).
3. **Importez les données** :
    
    * Via l'API (pour des gros volumes ou de l'automatisation).
4. **Automatisez le mapping** :
    
    * **Script Python** : Pour un contrôle total.
    * **OpenCode + MCP** : Pour des règles complexes ou dynamiques.




Exemple de calcul sur les colonnes

```python
import pandas as pd

# Lire le CSV
df = pd.read_csv("produits.csv")

# Calculer prix_ttc
df["prix_ttc"] = df["prix_ht"] * (1 + df["tva"] / 100)

# Renommer les colonnes si nécessaire
df.rename(columns={"Ref_Produit": "produit_id"}, inplace=True)

# Sauvegarder le CSV modifié
df.to_csv("produits_grist.csv", index=False)
```

**Avantages** :

* Simple et rapide pour des calculs basiques.
* Pas de dépendance à Grist.

**Inconvénients** :

* Si les données sources changent, il faut relancer le script.



---

### **Cas 1 : Générer un ID unique**

| **Pré-traitement (Python)** | `df["id"] = f"PROD-{df.index + 1}"` |

---

### **Cas 2 : Calculer un prix TTC**

| **Pré-traitement (Python)** | `df["prix_ttc"] = df["prix_ht"] * (1 + df["tva"] / 100)` |

---

### **Cas 3 : Mapping conditionnel**

**Exemple** : Si `categorie = "Premium"`, alors `coefficient = 1.2`, sinon `coefficient = 1.0`.

| **Pré-traitement (Python)** | `df["coefficient"] = df["categorie"].apply(lambda x: 1.2 if x == "Premium" else 1.0)` |

---

### **Cas 4 : Agrégation (ex: total par groupe)**

**Exemple** : Calculer le `total_prix` pour chaque groupe de produits.

| **Pré-traitement (Python)** | `df["total_prix"] = df.groupby("groupe")["prix_ht"].transform("sum")` | Nécessite `pandas`. |

---

## **5. Bonnes pratiques**

1. **Pour les calculs lourds** (ex: jointures, agrégations) :
    
    * **Préférez le pré-traitement** (Python, Node-RED) pour éviter de surcharger Grist.
2. **Pour les calculs dynamiques** (ex: mappings conditionnels) :
    
    * **Intégrez-les pendant l'import** (via l'API ou Node-RED) si ils dépendent de données externes.
    * **Utilisez des colonnes calculées** si ils dépendent uniquement de colonnes Grist.
3. **Pour les calculs temps réel** (ex: totaux, moyennes) :
    
    * **Utilisez des colonnes calculées** dans Grist pour une mise à jour automatique.
4. **Évitez les redondances** :
    
    * Si une colonne peut être calculée **à la volée** dans Grist (ex: `prix_ttc`), ne la stockez pas dans le CSV.
5. **Testez avec un petit jeu de données** :
    
    * Validez vos calculs sur un échantillon avant d'importer toutes les données.
6. **Documentez vos règles** :
    
    * Notez les formules de calcul dans un fichier ou un commentaire (ex: `// prix_ttc = prix_ht * (1 + tva/100)`).




---
## **7. Exemple avec OpenCode + MCP Grist**

Si vous utilisez **OpenCode** avec le **MCP Grist**, vous pouvez demander à l'IA de générer un script qui :

1. Lit le CSV.
2. Applique les calculs.
3. Envoie les données à Grist.

**Prompt pour OpenCode** :

> "Génère un script Python qui :
> 
> 1. Lit un CSV nommé `produits.csv` avec les colonnes : `Ref_Produit`, `Nom`, `prix_ht`, `tva`, `categorie`.
> 2. Ajoute les colonnes suivantes :
>     * `produit_id` : même valeur que `Ref_Produit`.
>     * `id` : un ID unique au format `PROD-{index}` (ex: `PROD-1`).
>     * `prix_ttc` : `prix_ht * (1 + tva/100)`.
>     * `coefficient` : `1.2` si `categorie == 'Premium'`, sinon `1.0`.
>     * `total` : `prix_ttc * coefficient`.
> 3. Envoie les données à l'API Grist (URL : `https://api.grist.com/api/docs/MON_DOC_ID/tables/produits/rows`, clé API : `MA_CLE_API`). 
>     Utilise les bibliothèques `pandas` et `requests`."

**Résultat attendu** :

```python
import pandas as pd
import requests

# Configuration
CSV_PATH = "produits.csv"
API_KEY = "MA_CLE_API"
DOC_ID = "MON_DOC_ID"
TABLE_NAME = "produits"

# Lire le CSV
df = pd.read_csv(CSV_PATH)

# Calculs
df["produit_id"] = df["Ref_Produit"]
df["id"] = [f"PROD-{i+1}" for i in range(len(df))]
df["prix_ttc"] = df["prix_ht"] * (1 + df["tva"] / 100)
df["coefficient"] = df["categorie"].apply(lambda x: 1.2 if x == "Premium" else 1.0)
df["total"] = df["prix_ttc"] * df["coefficient"]

# Sélectionner les colonnes à envoyer
data = df[["produit_id", "id", "Nom", "prix_ht", "tva", "prix_ttc", "coefficient", "total"]].to_dict("records")

# Envoyer à Grist
response = requests.post(
    f"https://api.grist.com/api/docs/{DOC_ID}/tables/{TABLE_NAME}/rows",
    json={"records": data},
    headers={"Authorization": f"Bearer {API_KEY}"}
)

print(response.json())
```

---





---

## **3. Structure recommandée pour votre projet Python**

Voici comment organiser votre projet pour **3-4 tables au départ**, avec une **évolutivité vers 10-15 tables**.

### **Arborescence du projet** :

```text
votre_projet/
│
├── data/
│   ├── input/               # CSV sources
│   │   ├── table1.csv
│   │   ├── table2.csv
│   │   └── ...
│   │
│   └── output/              # CSV traités (optionnel)
│       ├── table1_grist.csv
│       └── ...
│
├── scripts/
│   ├── config.py            # Configuration (clé API, DOC_ID, etc.)
│   ├── utils.py             # Fonctions utilitaires (renommage, calculs)
│   ├── import_table1.py     # Script pour table1
│   ├── import_table2.py     # Script pour table2
│   └── main.py              # Script principal (orchestre les imports)
│
├── requirements.txt         # Dépendances Python
└── README.md                # Documentation
```


---
## Avantages de cette structure

1. **Modularité** :
    * Chaque table a son **propre script**, ce qui rend le code **facile à maintenir**.
    * Les fonctions communes (renommage, calculs) sont dans `utils.py`.

2. **Évolutivité** :
    * Pour ajouter une **nouvelle table**, il suffit de :
        1. Créer un fichier `import_tableX.py`.
        2. Ajouter son mapping dans `config.py`.
        3. Ajouter un appel dans `main.py`.

3. **Réutilisabilité** :
    * Les fonctions dans `utils.py` peuvent être **réutilisées** pour toutes les tables.

4. **Testabilité** :
    * Vous pouvez tester chaque script **indépendamment** (ex: `python import_table1.py`).

5. **Documentation** :
    * Ajoutez des **commentaires** dans chaque fichier pour expliquer la logique.
    * Utilisez un `README.md` pour décrire le projet et comment l'exécuter.

---

### **Exemple de code modulaire**

#### **Fichier `config.py`** (configuration centrale) :

```python
# Clé API et ID du document Grist
GRIST_API_KEY = "votre_cle_api"
GRIST_DOC_ID = "votre_doc_id"

# Mapping des noms de colonnes (source -> Grist)
COLUMN_MAPPING = {
    "table1": {
        "Ref_Routes": "routes_id",
        "Nom": "name",
        "Prix_HT": "price_excl_tax"
    },
    "table2": {
        "Ref_Produits": "product_id",
        "Ref_Routes": "route_id"  # Clé étrangère
    }
}

# URL de base de l'API Grist
GRIST_API_URL = f"https://api.grist.com/api/docs/{GRIST_DOC_ID}"
```

---

#### **Fichier `utils.py`** (fonctions réutilisables) :

```python
import pandas as pd

def rename_columns(df, table_name, mapping):
    """Renomme les colonnes du DataFrame selon le mapping."""
    df = df.rename(columns=mapping[table_name])
    return df

def calculate_columns(df, table_name):
    """Applique les calculs spécifiques à une table."""
    if table_name == "table1":
        df["price_incl_tax"] = df["price_excl_tax"] * (1 + df["vat"] / 100)
        df["id"] = [f"T1-{i+1}" for i in range(len(df))]
    elif table_name == "table2":
        df["total_price"] = df["price_excl_tax"] * df["quantity"]
        df["id"] = [f"T2-{i+1}" for i in range(len(df))]
    return df

def validate_data(df, table_name):
    """Valide les données avant import (ex: pas de valeurs manquantes)."""
    if table_name == "table1":
        assert "routes_id" in df.columns, "Colonne routes_id manquante"
        assert not df["routes_id"].isna().any(), "Valeurs manquantes dans routes_id"
    return True
```

---

#### **Fichier `import_table1.py`** (script pour une table) :

```python
import pandas as pd
import requests
from config import GRIST_API_KEY, GRIST_API_URL, COLUMN_MAPPING
from utils import rename_columns, calculate_columns, validate_data

def import_table1(csv_path):
    # 1. Lire le CSV
    df = pd.read_csv(csv_path)

    # 2. Renommer les colonnes
    df = rename_columns(df, "table1", COLUMN_MAPPING)

    # 3. Appliquer les calculs
    df = calculate_columns(df, "table1")

    # 4. Valider les données
    validate_data(df, "table1")

    # 5. Envoyer à Grist
    url = f"{GRIST_API_URL}/tables/table1/rows"
    headers = {"Authorization": f"Bearer {GRIST_API_KEY}"}
    data = {"records": df.to_dict("records")}

    response = requests.post(url, json=data, headers=headers)
    return response.json()

if __name__ == "__main__":
    response = import_table1("data/input/table1.csv")
    print(response)
```

---

#### **Fichier `main.py`** (orchestration) :

```python
from import_table1 import import_table1
from import_table2 import import_table2
# Ajoutez d'autres imports ici

def main():
    # Importer toutes les tables
    print("Import de table1...")
    import_table1("data/input/table1.csv")

    print("Import de table2...")
    import_table2("data/input/table2.csv")

    # Ajoutez d'autres tables ici
    print("Tous les imports sont terminés !")

if __name__ == "__main__":
    main()
```

---

#### **Fichier `requirements.txt`** (dépendances) :

```text
pandas==2.0.3
requests==2.31.0
```

---

## **6. Comment gérer les liens entre tables (clés étrangères) ?**

Dans votre cas, vous devrez **lier vos tables importées** à des tables **déjà existantes dans Grist** (ex: `routes_id` dans votre CSV doit référencer une table `routes` déjà présente dans Grist).

### **Solutions** :

#### **A. Récupérer les IDs Grist existants avant l'import**

1. **Récupérez les données des tables existantes** via l'API Grist :
    
    python
    
    Copier
    
    ```
    def get_grist_table(api_key, doc_id, table_name):
        url = f"https://api.grist.com/api/docs/{doc_id}/tables/{table_name}/rows"
        headers = {"Authorization": f"Bearer {api_key}"}
        response = requests.get(url, headers=headers)
        return response.json()["records"]
    ```
    
2. **Créez un mapping** entre vos IDs sources et les IDs Grist :
    
    python
    
    Copier
    
    ```
    # Exemple : Récupérer les routes existantes dans Grist
    grist_routes = get_grist_table(GRIST_API_KEY, GRIST_DOC_ID, "routes")
    route_mapping = {route["old_id"]: route["id"] for route in grist_routes}
    ```
    
3. **Appliquez le mapping** à vos données avant l'import :
    
    python
    
    Copier
    
    ```
    df["routes_id"] = df["Ref_Routes"].map(route_mapping)
    ```
    

#### **B. Utiliser des colonnes de type "Reference" dans Grist**

* Dans Grist, assurez-vous que la colonne `routes_id` est de type **"Reference"** et pointe vers la table `routes`.
* L'API Grist gérera automatiquement les **liens entre tables** si vous envoyez les **bons IDs**.

#### **C. Importer d'abord les tables parents**

* Si vos tables ont des **dépendances** (ex: `orders` dépend de `users`), importez d'abord les tables **parents** (`users`), puis les tables **enfants** (`orders`).

---

## **7. Exemple complet avec Python pour 2 tables liées**

Supposons que vous ayez :

* Une table **existante dans Grist** : `routes` (avec une colonne `id`).
* Un CSV à importer : `orders.csv` (avec une colonne `Ref_Routes` qui doit être mappée à `routes_id` dans Grist).

### **Script Python** :

python

Copier

```
import pandas as pd
import requests
from config import GRIST_API_KEY, GRIST_DOC_ID, GRIST_API_URL

# 1. Récupérer les routes existantes dans Grist
def get_grist_routes():
    url = f"{GRIST_API_URL}/tables/routes/rows"
    headers = {"Authorization": f"Bearer {GRIST_API_KEY}"}
    response = requests.get(url, headers=headers)
    return {route["old_id"]: route["id"] for route in response.json()["records"]}

# 2. Lire et traiter le CSV orders
def import_orders(csv_path):
    df = pd.read_csv(csv_path)

    # Récupérer le mapping des routes
    route_mapping = get_grist_routes()

    # Renommer et mapper Ref_Routes -> routes_id
    df = df.rename(columns={"Ref_Routes": "routes_id"})
    df["routes_id"] = df["routes_id"].map(route_mapping)

    # Ajouter un ID unique pour les orders
    df["id"] = [f"ORDER-{i+1}" for i in range(len(df))]

    # Envoyer à Grist
    url = f"{GRIST_API_URL}/tables/orders/rows"
    headers = {"Authorization": f"Bearer {GRIST_API_KEY}"}
    data = {"records": df.to_dict("records")}

    response = requests.post(url, json=data, headers=headers)
    return response.json()

if __name__ == "__main__":
    import_orders("data/input/orders.csv")
```

---

## **8. Recommandations pour passer à 10-15 tables**

1. **Automatisez la création des tables** :
    
    * Utilisez l'API Grist pour **créer les tables dynamiquement** si elles n'existent pas :

```python
def create_table_if_not_exists(table_name, columns):
	url = f"{GRIST_API_URL}/tables"
	headers = {"Authorization": f"Bearer {GRIST_API_KEY}"}
	data = {
		"name": table_name,
		"columns": columns
	}
	response = requests.post(url, json=data, headers=headers)
	return response.json()
```
        
    * Exemple d'appel :

```python
create_table_if_not_exists("orders", [
	{"name": "id", "type": "Text"},
	{"name": "routes_id", "type": "Reference", "referencedTable": "routes"},
	{"name": "amount", "type": "Number"}
])
```
        
2. **Centralisez la configuration** :
    
    * Stockez **tous les mappings** (colonnes, calculs, dépendances) dans un fichier `config.py` ou un JSON.
    * Exemple :

```python
TABLE_CONFIG = {
	"orders": {
		"csv_path": "data/input/orders.csv",
		"column_mapping": {"Ref_Routes": "routes_id"},
		"calculations": ["id = ORDER-{index}"],
		"dependencies": ["routes"]  # Tables à importer avant
	},
	"products": {
		"csv_path": "data/input/products.csv",
		"column_mapping": {"Ref_Produits": "product_id"},
		"calculations": ["price_incl_tax = price_excl_tax * (1 + vat/100)"],
		"dependencies": []
	}
}
```
        
3. **Utilisez des classes pour les tables** :
    
    * Créez une **classe `TableImporter`** pour encapsuler la logique :
        
```python
class TableImporter:
	def __init__(self, name, config, api_key, doc_id):
		self.name = name
		self.config = config
		self.api_key = api_key
		self.doc_id = doc_id
		self.api_url = f"https://api.grist.com/api/docs/{doc_id}"

	def import_data(self):
		df = pd.read_csv(self.config["csv_path"])
		df = self.rename_columns(df)
		df = self.calculate_columns(df)
		self.send_to_grist(df)

	def rename_columns(self, df):
		return df.rename(columns=self.config["column_mapping"])

	def calculate_columns(self, df):
		# Appliquer les calculs définis dans config
		for calc in self.config["calculations"]:
			# Exemple : "price_incl_tax = price_excl_tax * (1 + vat/100)"
			exec(f"df['{calc.split('=')[0].strip()}'] = {calc.split('=')[1].strip()}")
		return df

	def send_to_grist(self, df):
		url = f"{self.api_url}/tables/{self.name}/rows"
		headers = {"Authorization": f"Bearer {self.api_key}"}
		data = {"records": df.to_dict("records")}
		response = requests.post(url, json=data, headers=headers)
		return response.json()
```
        
    * Utilisation :

```python
importer = TableImporter("orders", TABLE_CONFIG["orders"], GRIST_API_KEY, GRIST_DOC_ID)
importer.import_data()
```
        
4. **Gérez les dépendances entre tables** :
    
    * Importez les tables **dans le bon ordre** (parents avant enfants) :

```python
def import_all_tables():
	# D'abord les tables sans dépendances
	for table_name, config in TABLE_CONFIG.items():
		if not config["dependencies"]:
			importer = TableImporter(table_name, config, GRIST_API_KEY, GRIST_DOC_ID)
			importer.import_data()

	# Ensuite les tables avec dépendances
	for table_name, config in TABLE_CONFIG.items():
		if config["dependencies"]:
			importer = TableImporter(table_name, config, GRIST_API_KEY, GRIST_DOC_ID)
			importer.import_data()
```
        
5. **Ajoutez des logs et des validations** :
    
    * Utilisez la bibliothèque `logging` pour suivre l'exécution :

```python
import logging
logging.basicConfig(level=logging.INFO, filename="import.log")

def import_data(self):
	logging.info(f"Début de l'import pour {self.name}")
	try:
		df = pd.read_csv(self.config["csv_path"])
		# ... reste du code ...
		logging.info(f"Import réussi pour {self.name}")
	except Exception as e:
		logging.error(f"Erreur pour {self.name}: {e}")
```
        

---

## **9. Outils complémentaires pour Python**

| **Outil** | **Utilité** | **Exemple** |
| --- | --- | --- |
| **`pandas`** | Manipulation des données (CSV, calculs, renommage). | `df["prix_ttc"] = df["prix_ht"] * (1 + df["tva"] / 100)` |
| **`requests`** | Appels à l'API Grist. | `requests.post(url, json=data, headers=headers)` |
| **`sqlite3`** | Si vous devez faire des jointures locales avant l'import. | `pd.read_sql("SELECT * FROM table1 JOIN table2 ON ...", con)` |
| **`logging`** | Suivi des erreurs et des exécutions. | `logging.info("Import réussi")` |
| **`argparse`** | Pour rendre vos scripts configurables via la ligne de commande. | `parser.add_argument("--table", help="Nom de la table à importer")` |
| **`schedule`** | Planification des imports (alternative à `cron`). | `schedule.every().day.at("02:00").do(import_all_tables)` |
| **`pytest`** | Tests unitaires pour valider vos calculs. | `assert calculate_prix_ttc(100, 20) == 120` |

---

## **10. Exemple de script final optimisé**

Voici un **script Python complet** qui :

1. Gère **plusieurs tables**.
2. Applique des **mappings et calculs**.
3. Gère les **dépendances entre tables**.
4. Utilise des **logs et validations**.

python

Copier

```
import pandas as pd
import requests
import logging
from config import GRIST_API_KEY, GRIST_DOC_ID, TABLE_CONFIG

# Configuration du logging
logging.basicConfig(level=logging.INFO, filename="import.log", format="%(asctime)s - %(levelname)s - %(message)s")

class GristImporter:
    def __init__(self, api_key, doc_id):
        self.api_key = api_key
        self.doc_id = doc_id
        self.api_url = f"https://api.grist.com/api/docs/{doc_id}"
        self.tables_created = set()  # Suivi des tables déjà créées

    def create_table_if_not_exists(self, table_name, columns):
        """Crée une table dans Grist si elle n'existe pas."""
        if table_name in self.tables_created:
            return

        url = f"{self.api_url}/tables"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        data = {"name": table_name, "columns": columns}

        try:
            response = requests.post(url, json=data, headers=headers)
            response.raise_for_status()
            self.tables_created.add(table_name)
            logging.info(f"Table {table_name} créée ou déjà existante.")
        except requests.exceptions.RequestException as e:
            logging.error(f"Erreur lors de la création de la table {table_name}: {e}")
            raise

    def get_grist_table(self, table_name):
        """Récupère les données d'une table Grist existante."""
        url = f"{self.api_url}/tables/{table_name}/rows"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        response = requests.get(url, headers=headers)
        return response.json()["records"]

    def import_table(self, table_name, config):
        """Importe une table selon sa configuration."""
        logging.info(f"Début de l'import pour {table_name}")

        # 1. Lire le CSV
        try:
            df = pd.read_csv(config["csv_path"])
        except FileNotFoundError:
            logging.error(f"Fichier {config['csv_path']} introuvable.")
            return False

        # 2. Renommer les colonnes
        if "column_mapping" in config:
            df = df.rename(columns=config["column_mapping"])

        # 3. Appliquer les calculs
        if "calculations" in config:
            for calc in config["calculations"]:
                try:
                    # Exemple: "price_incl_tax = price_excl_tax * (1 + vat/100)"
                    col_name, expr = calc.split("=")
                    col_name = col_name.strip()
                    df[col_name] = df.eval(expr.strip())
                except Exception as e:
                    logging.error(f"Erreur dans le calcul '{calc}': {e}")
                    raise

        # 4. Gérer les dépendances (clés étrangères)
        if "dependencies" in config:
            for dep_table in config["dependencies"]:
                # Récupérer les données de la table dépendante dans Grist
                dep_data = self.get_grist_table(dep_table)
                dep_mapping = {row["old_id"]: row["id"] for row in dep_data}

                # Appliquer le mapping aux colonnes de dépendance
                for dep_col in config.get("dependency_columns", {}):
                    if dep_col in df.columns:
                        df[dep_col] = df[dep_col].map(dep_mapping)

        # 5. Créer la table si elle n'existe pas
        if "columns" in config:
            self.create_table_if_not_exists(table_name, config["columns"])

        # 6. Envoyer les données à Grist
        url = f"{self.api_url}/tables/{table_name}/rows"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        data = {"records": df.to_dict("records")}

        try:
            response = requests.post(url, json=data, headers=headers)
            response.raise_for_status()
            logging.info(f"Import réussi pour {table_name}: {len(df)} lignes.")
            return True
        except requests.exceptions.RequestException as e:
            logging.error(f"Erreur lors de l'import de {table_name}: {e}")
            return False

    def import_all(self):
        """Importe toutes les tables dans l'ordre des dépendances."""
        # D'abord les tables sans dépendances
        for table_name, config in TABLE_CONFIG.items():
            if not config.get("dependencies"):
                self.import_table(table_name, config)

        # Ensuite les tables avec dépendances
        for table_name, config in TABLE_CONFIG.items():
            if config.get("dependencies"):
                self.import_table(table_name, config)

if __name__ == "__main__":
    importer = GristImporter(GRIST_API_KEY, GRIST_DOC_ID)
    importer.import_all()
```

---

## **11. Configuration pour 3-4 tables (exemple)**

**Fichier `config.py`** :

python

Copier

```
GRIST_API_KEY = "votre_cle_api"
GRIST_DOC_ID = "votre_doc_id"

TABLE_CONFIG = {
    "routes": {
        "csv_path": "data/input/routes.csv",
        "column_mapping": {"Ref": "id", "Nom": "name"},
        "calculations": ["id = 'ROUTE-' + id.astype(str)"],
        "columns": [
            {"name": "id", "type": "Text"},
            {"name": "name", "type": "Text"}
        ],
        "dependencies": []
    },
    "products": {
        "csv_path": "data/input/products.csv",
        "column_mapping": {"Ref_Produits": "id", "Nom": "name", "Prix_HT": "price_excl_tax"},
        "calculations": [
            "price_incl_tax = price_excl_tax * (1 + vat / 100)",
            "id = 'PROD-' + id.astype(str)"
        ],
        "columns": [
            {"name": "id", "type": "Text"},
            {"name": "name", "type": "Text"},
            {"name": "price_excl_tax", "type": "Number"},
            {"name": "price_incl_tax", "type": "Number"},
            {"name": "vat", "type": "Number"}
        ],
        "dependencies": []
    },
    "orders": {
        "csv_path": "data/input/orders.csv",
        "column_mapping": {"Ref_Commandes": "id", "Ref_Produits": "product_id", "Ref_Routes": "route_id"},
        "calculations": [
            "total = price * quantity",
            "id = 'ORDER-' + id.astype(str)"
        ],
        "columns": [
            {"name": "id", "type": "Text"},
            {"name": "product_id", "type": "Reference", "referencedTable": "products"},
            {"name": "route_id", "type": "Reference", "referencedTable": "routes"},
            {"name": "price", "type": "Number"},
            {"name": "quantity", "type": "Number"},
            {"name": "total", "type": "Number"}
        ],
        "dependencies": ["products", "routes"],
        "dependency_columns": {
            "product_id": "Ref_Produits",  # Colonne source à mapper
            "route_id": "Ref_Routes"
        }
    }
}
```

---

## **12. Comment exécuter le script ?**

1. **Installer les dépendances** :
    
    bash
    
    Copier
    
    ```
    pip install pandas requests
    ```
    
2. **Placer vos CSV** dans `data/input/` (ex: `routes.csv`, `products.csv`).
    
3. **Exécuter le script** :
    
    bash
    
    Copier
    
    ```
    python main.py
    ```
    
4. **Planifier l'exécution** (optionnel) :
    
    * Avec `cron` (Linux/macOS) :
        
        bash
        
        Copier
        
        ```
        0 2 * * * /usr/bin/python3 /chemin/vers/main.py
        ```
        
    * Avec **Task Scheduler** (Windows) :
        * Créez une tâche qui exécute `python main.py` quotidiennement.

---

## **13. Avantages de cette approche Python**

✅ **Évolutif** : Ajoutez facilement de nouvelles tables en modifiant `TABLE_CONFIG`. 
✅ **Modulaire** : Chaque table a sa propre configuration. 
✅ **Robuste** : Gestion des erreurs et logging intégrés. 
✅ **Flexible** : Supporte des calculs complexes et des dépendances entre tables. 
✅ **Maintenable** : Code clair et documenté.

---

## **14. Quand choisir Node-RED à la place ?**

Même si **Python est recommandé**, voici les cas où **Node-RED pourrait être préférable** :

1. **Vous ou votre équipe n'êtes pas à l'aise avec Python**.
2. **Vous avez besoin d'une interface visuelle** pour collaborer avec des non-développeurs.
3. **Vos calculs sont simples** (renommage, calculs basiques) et ne nécessitent pas de logique complexe.
4. **Vous devez déclencher les imports via des événements externes** (ex: webhooks, fichiers, MQTT), et Node-RED est déjà utilisé dans votre écosystème.

---

## **15. Migration de Node-RED vers Python (si besoin)**

Si vous commencez avec **Node-RED** et que le projet grandit, vous pouvez :

1. **Exporter votre flux Node-RED** en JSON.
2. **Traduire la logique en Python** :
    * Les nœuds **"Function"** deviennent des fonctions Python.
    * Les nœuds **"CSV"** deviennent `pd.read_csv()`.
    * Les nœuds **"HTTP Request"** deviennent `requests.post()`.
3. **Utiliser les deux en parallèle** :
    * Node-RED pour les **déclencheurs** (ex: écouter un dossier pour de nouveaux CSV).
    * Python pour les **traitements lourds** (appelé via un nœud **"Exec"** dans Node-RED).

---

## **16. Résumé des prochaines étapes pour vous**

1. **Commencez avec Python** :
    
    * Créez la structure de projet décrite ci-dessus.
    * Implémentez l'import pour **1-2 tables** en premier (ex: `routes` et `products`).
    * Testez avec des **petits fichiers CSV**.
2. **Ajoutez les dépendances** :
    
    * Implémentez la logique pour gérer les **clés étrangères** (ex: `route_id` dans `orders`).
    * Vérifiez que les IDs correspondent entre vos CSV et Grist.
3. **Automatisez** :
    
    * Planifiez l'exécution du script avec `cron` ou **Task Scheduler**.
    * Ajoutez des **logs** pour suivre les imports.
4. **Évoluez** :
    
    * Ajoutez les **autres tables** une par une.
    * Optimisez le code si nécessaire (ex: paralléliser les imports avec `threading`).
5. **Documentez** :
    
    * Ajoutez des **commentaires** dans le code.
    * Rédigez un `README.md` avec :
        * La structure du projet.
        * Comment ajouter une nouvelle table.
        * Comment exécuter le script.

---

## **17. Exemple de `README.md`**

markdown

Copier

```
# Grist Data Importer

Ce projet permet d'importer des données depuis des fichiers CSV vers Grist, avec :
- Renommage des colonnes.
- Calculs personnalisés.
- Gestion des dépendances entre tables.

## Structure du projet
```

. 
├── data/  
│ ├── input/ # CSV sources  
│ └── output/ # CSV traités (optionnel)  
├── scripts/  
│ ├── config.py # Configuration  
│ ├── utils.py # Fonctions utilitaires  
│ └── main.py # Script principal  
├── requirements.txt # Dépendances Python  
└── README.md # Ce fichier

text

Copier

````
## Prérequis
- Python 3.8+
- Bibliothèques : `pandas`, `requests`

Installer les dépendances :
```bash
pip install -r requirements.txt
````

## Configuration

1. Modifiez `scripts/config.py` avec :
    
    * `GRIST_API_KEY` : Votre clé API Grist.
    * `GRIST_DOC_ID` : L'ID de votre document Grist.
    * `TABLE_CONFIG` : Configuration des tables à importer.
2. Placez vos fichiers CSV dans `data/input/`.
    

## Exécution

bash

Copier

```
python scripts/main.py
```

## Planification

Pour exécuter le script tous les jours à 2h :

bash

Copier

```
0 2 * * * /usr/bin/python3 /chemin/vers/scripts/main.py
```

## Ajouter une nouvelle table

1. Ajoutez un fichier CSV dans `data/input/`.
2. Ajoutez une entrée dans `TABLE_CONFIG` (dans `config.py`).
3. Si nécessaire, ajoutez des fonctions spécifiques dans `utils.py`.

text

Copier

```
---
---
## **18. Outils pour aller plus loin**
| **Besoin**             | **Outil/Technologie**             | **Utilité**                                                               |
|--------------------------|-------------------------------------|-----------------------------------------------------------------------------|
| **Gestion des secrets**  | `python-dotenv`                     | Stockez `GRIST_API_KEY` dans un fichier `.env` (sécurisé).               |
| **Tests unitaires**    | `pytest`                             | Testez vos fonctions de calcul et de mapping.                            |
| **Parallélisation**    | `concurrent.futures` ou `threading` | Importez plusieurs tables en parallèle pour gagner du temps.            |
| **Monitoring**         | `Prometheus` + `Grafana`             | Surveillez les imports (succès/échecs, temps d'exécution).              |
| **Containerisation**   | `Docker`                            | Exécutez votre script dans un conteneur pour une portabilité maximale.  |
| **Orchestration**      | `Apache Airflow`                    | Pour des workflows complexes avec dépendances entre tâches.            |

---
---
## **19. Exemple avec `python-dotenv` pour les secrets**
1. Installez la bibliothèque :
   ```bash
   pip install python-dotenv
```

2. Créez un fichier `.env` :
    
    env
    
    Copier
    
    ```
    GRIST_API_KEY=votre_cle_api
    GRIST_DOC_ID=votre_doc_id
    ```
    
3. Modifiez `config.py` :
    
    python
    
    Copier
    
    ```
    from dotenv import load_dotenv
    import os
    
    load_dotenv()  # Charge les variables du fichier .env
    
    GRIST_API_KEY = os.getenv("GRIST_API_KEY")
    GRIST_DOC_ID = os.getenv("GRIST_DOC_ID")
    ```
    

---

## **20. Conclusion : Python est le meilleur choix pour vous**

| **Critère** | **Pourquoi Python ?** |
| --- | --- |
| **Évolutivité** | Gère facilement 10-15 tables avec un code modulaire. |
| **Flexibilité** | Supporte tous types de calculs et de transformations. |
| **Maintenabilité** | Code versionnable, testable, et documentable. |
| **Performance** | Rapide et efficace pour le traitement de données. |
| **Intégration avec Grist** | L'API Grist est facile à utiliser avec `requests`. |
| **Automatisation** | Peut être planifié avec `cron` ou intégré dans des workflows plus larges. |

**Node-RED** reste une bonne option si :

* Vous **préférez une interface visuelle**.
* Vos **calculs sont simples**.
* Vous **collaborez avec des non-développeurs**.

Mais pour votre cas (10-15 tables, calculs personnalisés, évolutivité), **Python est clairement la meilleure solution**.


