Intégration Python

IsoFind s'intègre à Python de deux façons complémentaires : via le package open source isof disponible sur PyPI, qui permet de lire et vérifier des fichiers .isof dans n'importe quel script Python indépendamment d'IsoFind, et via l'API REST locale qui permet d'interroger et manipuler la base de données IsoFind directement depuis Python pendant qu'IsoFind est ouvert.

Le package isof (open source)

Package PyPI

Le package isof est un lecteur et vérificateur open source du format ISOF v1.0. Il est indépendant d'IsoFind : n'importe quel script Python peut lire un fichier .isof sans que le logiciel soit installé. Le code source est publié sous licence MIT.

pip install isof

Avec le support pandas (recommandé pour l'analyse de données) :

pip install isof[pandas]

Nécessite Python 3.9 ou supérieur.

Charger un fichier

import isof # Charger depuis un fichier report = isof.load("analyse_bolivie.isof") print(report) # <ISOfDocument v1.0 — 12 échantillon(s) — IGE Grenoble> # Charger depuis une chaîne JSON (depuis une API, une base de données...) with open("analyse.isof") as f: report = isof.loads(f.read())

Vérifier l'intégrité et l'authenticité

Le format ISOF supporte deux niveaux de signature. Le package vérifie les deux, hors ligne, sans connexion réseau : les certificats IsoFind Root CA et Issuing CA sont embarqués dans le package.

NiveauMécanismeGarantie
1 Hash SHA-256 sur le contenu Intégrité : le fichier n'a pas été modifié depuis l'export.
2 Signature ECDSA P-256 + PKI IsoFind Authenticité : les données ont été signées par un laboratoire certifié IsoFind.
# Vérification simple (bool) if report.is_authentic(): print(f"Données intègres. Signé par : {report.signature.signed_by}") else: print("Données altérées ou signature invalide.") # Résultat détaillé result = report.verify() print(result.valid) # bool print(result.level) # 0 (absente), 1 (SHA-256) ou 2 (ECDSA PKI) print(result.signer) # organisation ou CN du certificat print(result.signed_at) # horodatage ISO 8601 print(result.reason) # None si valide, message d'erreur sinon # Distinguer signature absente et signature corrompue if result.level == 0: print("Ce fichier n'est pas signé.") elif not result.valid: print(f"Signature présente mais invalide : {result.reason}")

Accéder aux données

# Échantillons print(len(report.samples)) for sample in report.samples: print(sample.name, sample.material_type, sample.classification) # Données isotopiques d'un échantillon for iso in report.samples[0].isotope_data: print(iso.element, iso.ratio_name, iso.ratio_value, iso.uncertainty) # Métadonnées du document print(report.created_by.organisation) print(report.created_at) print(report.project) # Méthodes documentées dans le fichier for key, method in report.methods.items(): print(key, method.get("label")) # Rendements de purification for key, yield_data in report.purification.items(): print(key, yield_data) # clé : "{sample_id}_{element}"

Export vers pandas et CSV

Le DataFrame produit par to_pandas() est au format "tidy" : une ligne par mesure isotopique, avec les métadonnées de l'échantillon dupliquées sur chaque ligne. Ce format est directement compatible avec matplotlib, seaborn, ou tout autre outil d'analyse.

df = report.to_pandas() # Colonnes disponibles selon le contenu du fichier df[["sample_name", "element", "ratio_name", "ratio_value", "ratio_2se"]] # Filtres standards pandas pb_data = df[df["element"] == "Pb"] sources = df[df["classification"] == "source"] sr_ratio = df[df["ratio_name"] == "87Sr/86Sr"] # Statistiques rapides sr_ratio["ratio_value"].describe() # Export CSV report.to_csv("export.csv") # équivalent à : report.to_pandas().to_csv("export.csv", index=False)

Parcourir les étapes d'un pipeline

for sample in report.samples: pipeline = report.get_pipeline(sample.id) if pipeline: print(f"{sample.name} — pipeline : {pipeline.label}") for stage in pipeline.stages: print(f" {stage.label} : {stage.status}")

Gestion des erreurs

from isof.exceptions import ISOfParseError, ISOfVersionError, ISOfSignatureError try: report = isof.load("fichier.isof") except ISOfVersionError as e: # Version du format non supportée par cette version du package print(f"Version {e.found} non supportée — mettez isof à jour.") except ISOfParseError as e: # Fichier JSON invalide ou structure incorrecte print(f"Fichier invalide : {e}") except ISOfSignatureError as e: # Erreur lors de la vérification cryptographique print(f"Erreur de signature : {e}")
Le package est disponible à pypi.org/project/isof et le code source sur https://github.com/ColinFerrari/isof. Le format ISOF est un standard ouvert : des implémentations tierces dans d'autres langages sont encouragées.

Interroger IsoFind depuis Python via l'API locale

API locale REST

Pendant qu'IsoFind est ouvert, son backend FastAPI est accessible à http://127.0.0.1:8001. Un script Python peut interroger cette API directement avec la bibliothèque requests pour lire des données, créer des échantillons ou déclencher des analyses depuis un notebook Jupyter, un script de traitement ou un pipeline de calcul externe.

pip install requests

Étape préalable : activer l'accès API

Par défaut, l'accès API externe est désactivé dans IsoFind. Avant d'utiliser un script Python, il faut activer l'accès depuis les préférences du logiciel :

Préférences Sécurité Accès API externe

Trois niveaux sont disponibles :

ModeComportementUsage recommandé
Désactivé Aucun script externe ne peut se connecter. /api/local/token retourne 403. Défaut. À conserver si aucun script externe n'est nécessaire.
Accès local sans confirmation Tout script depuis 127.0.0.1 obtient un token directement, sans popup. Poste personnel isolé, environnement de confiance totale.
Accès avec autorisation manuelle Chaque nouveau programme déclenche une popup dans IsoFind avec le chemin de l'exécutable. L'utilisateur valide ou refuse. "Se souvenir" enregistre le programme. Poste partagé, environnement professionnel, usage sensible.
En mode accès local sans confirmation, tout logiciel malveillant installé sur le poste peut lire et exporter l'intégralité de la base de données. N'activer ce mode que sur un poste dont vous contrôlez entièrement le contenu. En cas de doute, préférer le mode avec autorisation manuelle.
Onglet Sécurité des préférences IsoFind Figure 1 : Onglet Sécurité des préférences avec le sélecteur de mode d'accès API et la liste des programmes autorisés.

Obtenir le token de sécurité

Une fois l'accès activé, tout script Python récupère son token via une route dédiée accessible uniquement depuis 127.0.0.1. En mode avec autorisation manuelle, le premier appel attend (jusqu'à 60 secondes) que l'utilisateur valide la popup dans IsoFind.

import requests BASE = "http://127.0.0.1:8001" # Le script envoie son PID pour aider IsoFind à identifier l'exécutable import os r = requests.get( f"{BASE}/api/local/token", headers={"X-Client-PID": str(os.getpid())} ) # Si le mode est "désactivé", r.status_code == 403 # Si le mode est "autorisation manuelle" et que l'utilisateur n'a pas encore # répondu, la requête attend jusqu'à 60 secondes if r.status_code == 403: err = r.json().get("error", "") if err == "api_access_disabled": print("Accès API désactivé. Activez-le dans IsoFind > Préférences > Sécurité.") elif err == "authorization_denied": print("Accès refusé par l'utilisateur.") raise SystemExit(1) info = r.json() TOKEN = info.get("token") HEADERS = {"X-IsoFind-Token": TOKEN} if TOKEN else {} print(f"Connecté — mode : {info.get('mode', 'dev')}")
En mode autorisation manuelle avec "Se souvenir" coché, le programme est enregistré dans IsoFind. Les exécutions suivantes n'affichent plus de popup : le token est retourné immédiatement. Pour gérer les programmes enregistrés (voir, révoquer), aller dans Préférences > Sécurité > Programmes autorisés.
Un navigateur web malveillant ne peut pas accéder à /api/local/token : le middleware de validation de l'en-tête Host bloque toute requête dont l'hôte n'est pas 127.0.0.1:8001 ou localhost:8001. Un script Python sur le même poste envoie bien Host: 127.0.0.1:8001, il n'est pas affecté.

Vérifier qu'IsoFind est prêt

import requests BASE = "http://127.0.0.1:8001" r = requests.get(f"{BASE}/api/ready", headers=HEADERS) print(r.json()) # {'status': 'ready'}

Récupérer les échantillons

import requests import pandas as pd BASE = "http://127.0.0.1:8001" # Tous les échantillons samples = requests.get(f"{BASE}/api/samples", headers=HEADERS).json() print(f"{len(samples)} échantillons") # Avec tri et limite samples = requests.get( f"{BASE}/api/samples", params={"sort": "desc", "limit": 100} ).json() # Un échantillon précis s = requests.get(f"{BASE}/api/samples/42", headers=HEADERS).json() print(s["name"], s["material_type"]) # Version complète avec méthodes, publications et pipeline s_full = requests.get(f"{BASE}/api/v2/samples/42/full", headers=HEADERS).json() print(s_full["methods"])

Construire un DataFrame depuis la base IsoFind

import requests import pandas as pd BASE = "http://127.0.0.1:8001" # Récupérer toutes les données isotopiques d'un coup iso_data = requests.get(f"{BASE}/api/samples/isotopic-data", headers=HEADERS).json() df = pd.DataFrame(iso_data) # Colonnes disponibles : sample_id, sample_name, element, ratio_name, # ratio_value, uncertainty, standard_used, instrument, normalized, ... print(df.columns.tolist()) # Statistiques par élément print(df.groupby("element")["ratio_value"].describe()) # Filtrer sur les échantillons normalisés uniquement df_norm = df[df["normalized"] == True]

Créer un échantillon

r = requests.post( f"{BASE}/api/samples", headers=HEADERS, json={ "name": "BIF-2026-001", "material_type": "roche", "sector": "géologie", "collection_date": "2026-02-15", "collection_location":"Cerro Rico, Bolivie", "classification": "source", "latitude": -19.6078, "longitude": -65.7527, "project": "antimony-traceability-2026", } ) print(r.status_code, r.json())

Enregistrer des rendements de purification

sample_id = 42 # Enregistrer le rendement Sb pour cet échantillon r = requests.post( f"{BASE}/api/samples/{sample_id}/purification-yields", json={ "element": "Sb", "yield_percent": 87.3, "operator": "C. Ferrari", "method_key": "ion-exchange-sb-v2" } ) print(r.json()) # {'success': True, 'sample_id': 42, 'element': 'SB', 'yield_percent': 87.3} # Lire les rendements enregistrés yields = requests.get( f"{BASE}/api/samples/{sample_id}/purification-yields", headers=HEADERS ).json() print(yields)

Lancer une recherche de correspondances

# Analyse d'un échantillon déjà en base r = requests.post( f"{BASE}/analyze", headers=HEADERS, json={ "sample_id": 42, "threshold": 0.85, "algorithm": "hybrid", "classification_filter": "sources", "same_material": True, } ) result = r.json() print(f"{len(result['matches'])} correspondances") for match in result["matches"][:5]: print(f" #{match['rank']} {match['zone_name']} — score {match['overall_score']:.3f}") # Analyse manuelle sans échantillon en base r = requests.post( f"{BASE}/analyze/manual", headers=HEADERS, json={ "sample_name": "Inconnu-01", "material_type":"minerai", "threshold": 0.75, "isotope_data": [ {"element": "Sb", "ratio_name": "123Sb/121Sb", "ratio_value": 0.74738, "uncertainty": 0.00012}, {"element": "Pb", "ratio_name": "206Pb/204Pb", "ratio_value": 18.421, "uncertainty": 0.003}, ] } ) print(r.json()["matches"][:3])

Analyse par lots

# Analyser plusieurs échantillons en une seule requête r = requests.post( f"{BASE}/api/matching/batch", json=[1, 2, 3, 42, 57], headers=HEADERS ) results = r.json() for sample_id, res in results.items(): if res["status"] == "success": print(f"ID {sample_id} : {res['matches_count']} correspondances, " f"meilleure zone = {res['best_zone']} (score {res['best_score']:.3f})") else: print(f"ID {sample_id} : erreur — {res['error']}")

Exporter en CSV depuis Python

import requests # Télécharger le CSV complet de la base r = requests.get(f"{BASE}/api/samples/export", headers=HEADERS) with open("export.csv", "wb") as f: f.write(r.content) # Export groupé avec toutes les données associées (méthodes, publications) r = requests.post( f"{BASE}/api/v2/samples/export-batch", json=[1, 2, 3] ) data = r.json()["data"] print(f"{data['count']} échantillons exportés le {data['exported_at']}")

Interroger les CRM et les shifts

# Liste des CRM disponibles crms = requests.get(f"{BASE}/api/crm/list", headers=HEADERS).json()["data"] print(f"{len(crms)} CRM en base") # Calcul du shift pour un CRM donné r = requests.post( f"{BASE}/api/shifts/calculate/NIST-3102a", params={ "element": "Sb", "isotope_ratio":"123Sb/121Sb" } ) shift = r.json() if shift["success"]: print(f"Shift calculé : {shift['data']}") # Convertir une valeur isotopique d'un standard à un autre r = requests.post( f"{BASE}/api/shifts/convert", json={ "value": 0.74738, "from_std": "NIST-3102a", "to_std": "IAEA-SbS-1", "element": "Sb", "ratio_name": "123Sb/121Sb" } ) print(r.json())
La documentation interactive Swagger est disponible à http://127.0.0.1:8001/docs pendant qu'IsoFind est ouvert. Elle liste toutes les routes avec leurs schémas JSON complets et permet de les tester directement depuis le navigateur, utile pour explorer de nouvelles routes avant de les intégrer dans un script.

Combiner les deux approches

Toutes les fonctions auxiliaires définies ci-dessous supposent que BASE et HEADERS sont définis en tête de script via GET /api/local/token. Dans un notebook Jupyter, exécuter la cellule d'initialisation une seule fois en début de session.

Le workflow le plus puissant combine l'API locale (pour accéder aux données de la base IsoFind en direct) et le package isof (pour lire et vérifier les fichiers exportés). Exemple complet : exporter un ensemble d'échantillons depuis IsoFind, signer le fichier ISOF résultant et le vérifier programmatiquement.

import requests import isof import pandas as pd BASE = "http://127.0.0.1:8001" # 1. Récupérer les échantillons depuis la base IsoFind iso_data = requests.get(f"{BASE}/api/samples/isotopic-data", headers=HEADERS).json() df = pd.DataFrame(iso_data) # 2. Analyser : moyennes par élément et par matériau pivot = df.pivot_table( values="ratio_value", index="sample_material_type", columns="element", aggfunc="mean" ) print(pivot) # 3. Charger et vérifier un fichier ISOF exporté par IsoFind report = isof.load("export_certifie.isof") result = report.verify() if result.valid and result.level == 2: print(f"Certifié par : {result.signer}") print(f"Signé le : {result.signed_at}") df_certified = report.to_pandas() else: raise ValueError(f"Signature invalide : {result.reason}")

Console Python intégrée dans IsoFind

Console Pyodide

En plus des deux approches ci-dessus, IsoFind intègre une console Python interactive qui s'exécute directement dans le logiciel, sans installation. Elle est propulsée par Pyodide (Python 3.11 compilé en WebAssembly) et donne accès aux données de la base courante via des variables et fonctions prêtes à l'emploi.

La console est accessible depuis :

Menu Outils Console Python
Console Python intégrée dans IsoFind Figure 2 : Console Python IsoFind avec les données de la base chargées automatiquement.

Variables disponibles automatiquement

# Chargées au démarrage depuis /api/samples samples # list[dict] : tous les échantillons de la base isotopes # dict : données isotopiques indexées par nom d'échantillon

Fonctions utilitaires pré-définies

# Accès par identifiant ou par nom s = get_sample(42) s = get_sample_by_name("BIF-2026-001") iso = get_isotopes("BIF-2026-001") # Conversion en DataFrame pandas (une ligne par mesure isotopique) df = to_dataframe() print(df.head()) # Statistiques pour un ratio isotopique isotope_stats("87Sr/86Sr") Statistics for 87Sr/86Sr: Count: 42 Mean: 0.710341 Std: 0.002187 Min: 0.706012 Max: 0.715892 Median: 0.710215

Bibliothèques disponibles

import numpy as np import pandas as pd # Exemple : distribution des valeurs 87Sr/86Sr vals = [ iso.get("ratio_value") for s in samples for iso in (s.get("isotope_data") or []) if iso.get("ratio_name") == "87Sr/86Sr" and iso.get("ratio_value") is not None ] arr = np.array(vals, dtype=float) print(f"n={len(arr)}, mean={arr.mean():.6f}, std={arr.std():.6f}") # Exemple : filtrage et tri avec pandas df = to_dataframe() roches = df[df["material_type"] == "roche"].sort_values("87Sr/86Sr") print(roches[["name", "87Sr/86Sr", "87Sr/86Sr_err"]])
Matplotlib n'est pas disponible dans la console Pyodide (pas de rendu graphique dans l'environnement navigateur). Pour des visualisations, utiliser df.describe() et isotope_stats() pour les statistiques, ou exporter les données et les tracer dans un script Python externe.

Récapitulatif des trois approches

Approche Quand l'utiliser IsoFind requis Installation
Package isof (PyPI) Lire et vérifier des fichiers .isof dans n'importe quel script ou pipeline, indépendamment d'IsoFind. Partager des données vérifiables avec des tiers. Non pip install isof
API locale (requests) Accéder à la base IsoFind en direct, créer des échantillons, déclencher des analyses depuis un notebook Jupyter ou un script de traitement externe. Nécessite d'activer l'accès dans Préférences > Sécurité. Oui (ouvert) pip install requests
Console Pyodide intégrée Exploration rapide, calculs ad hoc, statistiques sur la base courante. Pas d'installation, directement dans IsoFind. Oui (ouvert) Aucune