Python Integration

IsoFind integrates with Python in two complementary ways: via the open-source isof package available on PyPI, which allows reading and verifying .isof files in any Python script independently of IsoFind, and via the local REST API, which allows querying and manipulating the IsoFind database directly from Python while IsoFind is open.

The isof package (open source)

PyPI Package

The isof package is an open-source reader and validator for the ISOF v1.0 format. It is independent of IsoFind: any Python script can read an .isof file without the software being installed. The source code is published under the MIT license.

pip install isof

With pandas support (recommended for data analysis):

pip install isof[pandas]

Requires Python 3.9 or higher.

Loading a file

import isof # Load from a file report = isof.load("bolivia_analysis.isof") print(report) # <ISOfDocument v1.0 — 12 sample(s) — IGE Grenoble> # Load from a JSON string (from an API, database, etc.) with open("analysis.isof") as f: report = isof.loads(f.read())

Verifying integrity and authenticity

The ISOF format supports two levels of signature. The package verifies both offline, without a network connection: IsoFind Root CA and Issuing CA certificates are embedded in the package.

LevelMechanismGuarantee
1 SHA-256 Hash of content Integrity: the file has not been modified since export.
2 ECDSA P-256 Signature + IsoFind PKI Authenticity: data was signed by an IsoFind certified laboratory.
# Simple verification (bool) if report.is_authentic(): print(f"Data intact. Signed by: {report.signature.signed_by}") else: print("Data altered or signature invalid.") # Detailed result result = report.verify() print(result.valid) # bool print(result.level) # 0 (none), 1 (SHA-256) or 2 (ECDSA PKI) print(result.signer) # organization or CN of the certificate print(result.signed_at) # ISO 8601 timestamp print(result.reason) # None if valid, error message otherwise # Distinguishing missing from corrupted signature if result.level == 0: print("This file is not signed.") elif not result.valid: print(f"Signature present but invalid: {result.reason}")

Accessing data

# Samples print(len(report.samples)) for sample in report.samples: print(sample.name, sample.material_type, sample.classification) # Isotopic data for a sample for iso in report.samples[0].isotope_data: print(iso.element, iso.ratio_name, iso.ratio_value, iso.uncertainty) # Document metadata print(report.created_by.organisation) print(report.created_at) print(report.project) # Methods documented in the file for key, method in report.methods.items(): print(key, method.get("label")) # Purification yields for key, yield_data in report.purification.items(): print(key, yield_data) # key: "{sample_id}_{element}"

Exporting to pandas and CSV

The DataFrame produced by to_pandas() is in "tidy" format: one row per isotopic measurement, with sample metadata duplicated on each row. This format is directly compatible with matplotlib, seaborn, or any other analysis tool.

df = report.to_pandas() # Available columns depending on file content df[["sample_name", "element", "ratio_name", "ratio_value", "ratio_2se"]] # Standard pandas filters pb_data = df[df["element"] == "Pb"] sources = df[df["classification"] == "source"] sr_ratio = df[df["ratio_name"] == "87Sr/86Sr"] # Quick statistics sr_ratio["ratio_value"].describe() # CSV Export report.to_csv("export.csv") # equivalent to: report.to_pandas().to_csv("export.csv", index=False)

Browsing pipeline stages

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}")

Error handling

from isof.exceptions import ISOfParseError, ISOfVersionError, ISOfSignatureError try: report = isof.load("file.isof") except ISOfVersionError as e: # Format version not supported by this package version print(f"Version {e.found} not supported — update isof.") except ISOfParseError as e: # Invalid JSON file or incorrect structure print(f"Invalid file: {e}") except ISOfSignatureError as e: # Error during cryptographic verification print(f"Signature error: {e}")
The package is available at pypi.org/project/isof and the source code at https://github.com/ColinFerrari/isof. The ISOF format is an open standard: third-party implementations in other languages are encouraged.

Querying IsoFind from Python via Local API

Local REST API

While IsoFind is open, its FastAPI backend is accessible at http://127.0.0.1:8001. A Python script can query this API directly using the requests library to read data, create samples, or trigger analyses from a Jupyter notebook, processing script, or external calculation pipeline.

pip install requests

Preliminary Step: Enabling API Access

By default, external API access is disabled in IsoFind. Before using a Python script, access must be enabled from the software preferences:

Preferences Security External API Access

Three levels are available:

ModeBehaviorRecommended Usage
Disabled No external script can connect. /api/local/token returns 403. Default. Keep if no external script is needed.
Local access without confirmation Any script from 127.0.0.1 gets a token directly, without a popup. Isolated personal workstation, environment of total trust.
Access with manual authorization Each new program triggers a popup in IsoFind with the executable path. The user approves or refuses. "Remember" saves the program. Shared workstation, professional environment, sensitive usage.
In local access without confirmation mode, any malicious software installed on the workstation can read and export the entire database. Only activate this mode on a machine where you fully control the content. When in doubt, prefer the manual authorization mode.
IsoFind Preferences Security Tab Figure 1: Security tab in preferences with API access mode selector and list of authorized programs.

Obtaining the Security Token

Once access is enabled, any Python script retrieves its token via a dedicated route accessible only from 127.0.0.1. In manual authorization mode, the first call waits (up to 60 seconds) for the user to validate the popup in IsoFind.

import requests BASE = "http://127.0.0.1:8001" # The script sends its PID to help IsoFind identify the executable import os r = requests.get( f"{BASE}/api/local/token", headers={"X-Client-PID": str(os.getpid())} ) # If mode is "disabled", r.status_code == 403 # If mode is "manual authorization" and user hasn't responded yet, # the request waits up to 60 seconds if r.status_code == 403: err = r.json().get("error", "") if err == "api_access_disabled": print("API Access disabled. Enable it in IsoFind > Preferences > Security.") elif err == "authorization_denied": print("Access denied by user.") raise SystemExit(1) info = r.json() TOKEN = info.get("token") HEADERS = {"X-IsoFind-Token": TOKEN} if TOKEN else {} print(f"Connected — mode: {info.get('mode', 'dev')}")
In manual authorization mode with "Remember" checked, the program is saved in IsoFind. Subsequent executions no longer display a popup: the token is returned immediately. To manage saved programs (view, revoke), go to Preferences > Security > Authorized Programs.
A malicious web browser cannot access /api/local/token: the Host header validation middleware blocks any request where the host is not 127.0.0.1:8001 or localhost:8001. A Python script on the same machine correctly sends Host: 127.0.0.1:8001 and is not affected.

Verifying that IsoFind is ready

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

Retrieving samples

import requests import pandas as pd BASE = "http://127.0.0.1:8001" # All samples samples = requests.get(f"{BASE}/api/samples", headers=HEADERS).json() print(f"{len(samples)} samples") # With sorting and limit samples = requests.get( f"{BASE}/api/samples", params={"sort": "desc", "limit": 100} ).json() # A specific sample s = requests.get(f"{BASE}/api/samples/42", headers=HEADERS).json() print(s["name"], s["material_type"]) # Full version with methods, publications, and pipeline s_full = requests.get(f"{BASE}/api/v2/samples/42/full", headers=HEADERS).json() print(s_full["methods"])

Building a DataFrame from the IsoFind database

import requests import pandas as pd BASE = "http://127.0.0.1:8001" # Retrieve all isotopic data at once iso_data = requests.get(f"{BASE}/api/samples/isotopic-data", headers=HEADERS).json() df = pd.DataFrame(iso_data) # Available columns: sample_id, sample_name, element, ratio_name, # ratio_value, uncertainty, standard_used, instrument, normalized, ... print(df.columns.tolist()) # Statistics by element print(df.groupby("element")["ratio_value"].describe()) # Filter for normalized samples only df_norm = df[df["normalized"] == True]

Creating a sample

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

Recording purification yields

sample_id = 42 # Record Sb yield for this sample 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} # Read recorded yields yields = requests.get( f"{BASE}/api/samples/{sample_id}/purification-yields", headers=HEADERS ).json() print(yields)

Running a matching search

# Analyze a sample already in the database 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'])} matches found") for match in result["matches"][[:5]]: print(f" #{match['rank']} {match['zone_name']} — score {match['overall_score']:.3f}") # Manual analysis without sample in database r = requests.post( f"{BASE}/analyze/manual", headers=HEADERS, json={ "sample_name": "Unknown-01", "material_type":"ore", "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]])

Batch Analysis

# Analyze multiple samples in a single request 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']} matches, " f"best zone = {res['best_zone']} (score {res['best_score']:.3f})") else: print(f"ID {sample_id}: error — {res['error']}")

Exporting to CSV from Python

import requests # Download complete database CSV r = requests.get(f"{BASE}/api/samples/export", headers=HEADERS) with open("export.csv", "wb") as f: f.write(r.content) # Grouped export with all associated data (methods, publications) r = requests.post( f"{BASE}/api/v2/samples/export-batch", json=[1, 2, 3] ) data = r.json()["data"] print(f"{data['count']} samples exported on {data['exported_at']}")

Querying CRMs and shifts

# List available CRMs crms = requests.get(f"{BASE}/api/crm/list", headers=HEADERS).json()["data"] print(f"{len(crms)} CRMs in database") # Calculate shift for a given CRM 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"Calculated shift: {shift['data']}") # Convert an isotopic value from one standard to another 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())
Interactive Swagger documentation is available at http://127.0.0.1:8001/docs while IsoFind is open. It lists all routes with their full JSON schemas and allows testing them directly from the browser, which is useful for exploring new routes before integrating them into a script.

Combining Both Approaches

All helper functions defined below assume that BASE and HEADERS are defined at the top of the script via GET /api/local/token. In a Jupyter notebook, run the initialization cell once at the start of the session.

The most powerful workflow combines the local API (to access live IsoFind database data) and the isof package (to read and verify exported files). Full example: export a set of samples from IsoFind, sign the resulting ISOF file, and verify it programmatically.

import requests import isof import pandas as pd BASE = "http://127.0.0.1:8001" # 1. Retrieve samples from the IsoFind database iso_data = requests.get(f"{BASE}/api/samples/isotopic-data", headers=HEADERS).json() df = pd.DataFrame(iso_data) # 2. Analyze: means by element and material pivot = df.pivot_table( values="ratio_value", index="sample_material_type", columns="element", aggfunc="mean" ) print(pivot) # 3. Load and verify an ISOF file exported by IsoFind report = isof.load("certified_export.isof") result = report.verify() if result.valid and result.level == 2: print(f"Certified by: {result.signer}") print(f"Signed on: {result.signed_at}") df_certified = report.to_pandas() else: raise ValueError(f"Invalid signature: {result.reason}")

Built-in Python Console in IsoFind

Pyodide Console

In addition to the two approaches above, IsoFind includes an interactive Python console that runs directly within the software, with no installation required. It is powered by Pyodide (Python 3.11 compiled to WebAssembly) and provides access to current database data via ready-to-use variables and functions.

The console is accessible from:

Tools Menu Python Console
Built-in Python console in IsoFind Figure 2: IsoFind Python console with database data loaded automatically.

Automatically Available Variables

# Loaded at startup from /api/samples samples # list[dict]: all samples in the database isotopes # dict: isotopic data indexed by sample name

Pre-defined Utility Functions

# Access by ID or name s = get_sample(42) s = get_sample_by_name("BIF-2026-001") iso = get_isotopes("BIF-2026-001") # Convert to pandas DataFrame (one row per isotopic measurement) df = to_dataframe() print(df.head()) # Statistics for an isotope ratio 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

Available Libraries

import numpy as np import pandas as pd # Example: distribution of 87Sr/86Sr values 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}") # Example: filtering and sorting with pandas df = to_dataframe() rocks = df[df["material_type"] == "rock"].sort_values("87Sr/86Sr") print(rocks[["name", "87Sr/86Sr", "87Sr/86Sr_err"]])
Matplotlib is not available in the Pyodide console (no graphical rendering in the browser environment). For visualizations, use df.describe() and isotope_stats() for statistics, or export the data and plot it in an external Python script.

Summary of the Three Approaches

Approach When to use it IsoFind Required Installation
isof package (PyPI) Read and verify .isof files in any script or pipeline, independently of IsoFind. Share verifiable data with third parties. No pip install isof
Local API (requests) Access the live IsoFind database, create samples, trigger analyses from a Jupyter notebook or external processing script. Requires enabling access in Preferences > Security. Yes (Running) pip install requests
Built-in Pyodide Console Quick exploration, ad hoc calculations, statistics on the current database. No installation, directly inside IsoFind. Yes (Running) None