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

Python console embedded in IsoFind

Pyodide console

In addition to the two approaches above, IsoFind ships an interactive Python console that runs directly inside the software, with no installation. It is powered by Pyodide (Python 3.11 compiled to WebAssembly) and shares the same data source as the rest of the application: samples and their isotopic measurements are loaded automatically at startup. It supports exploration, statistical computation, plotting with matplotlib, local CSV export, and, when the edition allows it, access to the Nexus engine.

The console is available from:

Tools menu Python Console
Python console embedded in IsoFind Figure 2: IsoFind Python Console with the database loaded automatically.

Variables available automatically

# Loaded at startup from the current database samples # list[dict]: every sample in the database analyses # list[dict]: one row per isotopic measurement # (sample_id, sample_name, element, isotope_ratio, ratio_value, uncertainty) isotopes # dict: isotopic data indexed by sample name
The console reads data through the same internal source as the application: what you see on screen and what the console sees are always consistent. After an import or an edit, reload_data() refreshes samples, analyses and isotopes without reloading the page.

Predefined helper functions

# Access by id or by name s = get_sample(42) s = get_sample_by_name("BIF-2026-001") iso = get_isotopes("BIF-2026-001") # Conversion to a pandas DataFrame df = to_dataframe() # one row per sample, columns = isotopic ratios measurements = analyses_df() # one row per isotopic measurement (tidy format) # Statistics for an isotopic 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 # Reload data from the current database reload_data() # Export a DataFrame to CSV (local download) export_csv(df, "samples.csv")
When the result of a command is a DataFrame, the console renders it directly as a table. A simple df.head() or analyses_df() is enough to inspect the data.

Plotting with matplotlib

matplotlib is available in the console. Any figure opened while a command runs is rendered automatically in the output, then closed. No explicit plt.show() call is needed.

import matplotlib.pyplot as plt df = to_dataframe() plt.figure(figsize=(6, 4)) plt.hist(df["87Sr/86Sr"].dropna(), bins=20) plt.xlabel("87Sr/86Sr") plt.ylabel("count") plt.title("87Sr/86Sr distribution") # The figure is displayed automatically below the command.
matplotlib is sizeable, so it loads in the background right after the console opens. A message confirms when it is ready; for the first few seconds, only non-graphical computation is available.

Access to the Nexus engine (edition dependent)

The console exposes the Nexus engine through the nexus object, provided the installed edition ships the Nexus module. At startup, the console checks backend availability and only enables the object if it responds. Editions without Nexus, IsoFind Essential in particular, display "Nexus unavailable in this edition" and any call raises an explicit error.

Nexus calls query the backend, so they are asynchronous and used with await:

# Available only if the edition ships Nexus stats = await nexus.statistics() print(stats) # Query the fractionation database res = await nexus.query({"element": "Sb", "limit": 20}) # Supported elements and process types elements = await nexus.elements() processes = await nexus.processes() # Analyse a single process res = await nexus.analyze({ "sourceSignature": 0.0, "productSignature": -1.2, "conditions": {"pH": 7.0, "Eh": 200, "temperature": 25, "element": "Sb"}, "tolerance": 1.0, "topN": 10 }) # Candidate process chain res = await nexus.chain({ "sourceSignature": 0.0, "productSignature": -1.2, "conditions": {"pH": 7.0, "element": "Sb"}, "maxChainLength": 3 })
Nexus access from the console is determined by the installed edition, that is, by the presence of the Nexus module on the backend. It cannot be configured from the console: an edition without Nexus cannot enable it this way.

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("isotope_ratio") == "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"] == "roche"].sort_values("87Sr/86Sr") print(rocks[["name", "87Sr/86Sr", "87Sr/86Sr_err"]])
The console keeps a command history: the up and down arrows recall previous commands. help() lists the available variables, functions and the Nexus status at any time.

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