Python Client 201: Scripting Your First Digital Thread
In Platform 101 you used the browser to register a spreadsheet, run an extraction, add a new version, extract again, and compare what changed.
Here you will do the same flow with a short Python script: one run registers the file, runs two extractions, saves the extracted named_cells from each pass, and produces a comparison — same files, same revisions you see in the UI, but now the extracted data is in your hands as JSON to apply your own logic.
Run the script end to end once (it only takes a minute or two to finish). Then use Understand the script for a short walkthrough of each part and how it maps to the platform.
By the end you will know how to:
- Connect to the Istari Digital Platform from Python (acting as your user)
- Register a file, run a job, and wait for it to finish
- List and download extracted resources
- Add a new revision and run extraction again
- Apply your own logic to the extracted data while staying anchored to the file and revisions recorded by the platform
Time: ~15 minutes.
Prerequisites
- An Istari Digital Platform account. If you don't have one yet, follow the Sign-up Guide.
- Work through Platform 101 first so the UI ideas (model, job, resources, versions) feel familiar.
- The registry URL and a Personal Access Token for the Istari Digital Platform you are using (we set them as environment variables in the next section). For token help, see Personal Access Tokens.
- An agent with the Open Spreadsheet integration and access to run
@istari:extract— your admin can confirm; see Manage Tool Access.
Sample files (same as Platform 101). Save both in a folder, for example ~/uas-tutorial/:
- Group3-UAS-Requirements.xlsx — baseline
- Group3-UAS-Requirements-v2.xlsx — after a design change
Set up your environment
Install the client
We use uv so the same commands work on every machine:
curl -LsSf https://astral.sh/uv/install.sh | sh # if you do not have uv yet
mkdir -p ~/uas-tutorial && cd ~/uas-tutorial
# Pin the venv to a Python version supported by the client.
# If 3.12 is not already on your machine, uv downloads a managed CPython build for you.
uv venv --python 3.12
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\Activate.ps1 # Windows (PowerShell) — uncomment instead of the line above
uv pip install istari-digital-client
--python?uv venv --python 3.12 tells uv to use Python 3.12 specifically; if it is not on your machine, uv downloads a managed CPython build and uses that — no pyenv or brew install python needed.
If you'd rather use python -m venv or pyenv directly, make sure your interpreter matches the client's supported versions.
Point the script at your platform
- In the Istari Digital Platform you are using, open Settings → Developer Settings.
- Registry URL — copy the registry URL for that platform, then in your same terminal (where the venv is active) run:
export ISTARI_REGISTRY_URL="https://...paste the registry URL here..." - Personal Access Token — create or copy a token in Developer Settings, then:
Treat the token as a secret — it grants API access as you: do not commit it to git, post it in chat, or share screenshots that show the value.
export ISTARI_PERSONAL_ACCESS_TOKEN="...paste your token here..."
On Windows, use set VAR=value instead of export.
Run the script
- In the folder that contains the two
.xlsxfiles, save tutorial201.py there (easiest). The full script is also reproduced below in Full script if you prefer to read it inline or paste into a new file. - The script uses
assertto catch a few common mistakes up front: the two.xlsxfiles must be in the current folder, and the registry URL and token environment variables must be set in the same terminal. If Python raisesAssertionError, read the message—it points to what to fix. - Run:
python tutorial201.py
What you should see
In the terminal: Model and revision IDs, two jobs that reach Completed, two lists of resources, then writes of named_cells_rev1.json and named_cells_rev2.json,
followed by a summary (file name, model id, from / to revision ids and times) and a Change detail list of the named cells that changed (name: before → after)
— output tied to the same source of truth as the platform, not just anonymous JSON.
A trimmed example of what scrolls past (your IDs, timestamps, and values will differ):
healthy=True checks=[CheckResult(name='database', passed=True, details=None), CheckResult(name='storage', passed=True, details=None)]
Model registered: Group3-UAS-Requirements.xlsx
Model ID: 1e59b631-bf46-4945-ad4e-271803add9d2
Revision ID: c4a5b113-72b0-4666-b351-0b1cdfea4f61
Job: 8e092739-3712-4434-8f3c-659f99ebfc00
status: Pending
status: Running
status: Completed
First extraction done.
Resources produced by job 1 (6 items):
- named_cells.json (file=4ab2… rev=9f01…)
- worksheet_data.json (file=5cd3… rev=a112…)
- workbook.html (file=6ef4… rev=b223…)
- …workbook.pdf, workbook.xlsx, run logs (each with their own file/rev ids)
Saved …/named_cells_rev1.json (rev 9f01…)
Second file uploaded (new revision).
Model ID: 1e59b631-bf46-4945-ad4e-271803add9d2 (same as before)
Revision ID: 279cd9f6-cbc8-4dc4-8205-07a64df4e9e1
Job: b98c655d-a080-4925-ab4d-b3b4b61d00e2
status: Claimed
status: Running
status: Completed
Second extraction done.
Resources produced by job 2 (6 items):
- named_cells.json (file=4ab2… rev=c334…) ← same file id as run 1, new revision id
- worksheet_data.json (file=5cd3… rev=d445…) ← same file id, new revision
- …same kinds as above; in each row file= matches run 1 and rev= is new
Saved …/named_cells_rev2.json (rev c334…)
--- Summary (file and revisions from the platform) ---
Named parameters (named_cells) for 'Group3-UAS-Requirements.xlsx' — model 1e59b631-bf46-4945-ad4e-271803add9d2
from: revision c4a5b113-72b0-4666-b351-0b1cdfea4f61 · 2026-04-24T00:27:43+00:00 · user 80ddca7e-…
to: revision 279cd9f6-cbc8-4dc4-8205-07a64df4e9e1 · 2026-04-24T00:28:08+00:00 · user 80ddca7e-…
Change detail (8 named cell(s) changed):
- SubTitle: …Rev A… → …Rev B…
- control_signal_value: 60000.0 → 80000.0
- cruise_speed_value: 100.0 → 110.0
- max_weight_value: 275.0 → 310.0
- position_accuracy_value: 2.5 → 1.5
- range_value: 1000.0 → 1200.0
- temperature_value: -10 to +45 → -20 to +50
- video_tx_value: 50000.0 → 80000.0
The two Model ID lines are identical while the Revision IDs differ — the same logical file moving through two versions, exactly as you'd see it on the Versions tab in the UI.
The same pattern shows up for each artifact: in the per‑job listings, the file= of named_cells.json (and every other artifact name) matches across run 1 and run 2, while the rev= is new in run 2 — proof that the second extraction added a new revision to the same artifact file, not a brand‑new file.
The Change detail list is what a developer ultimately wants: just the named cells whose values moved between those two revisions.
On disk (next to the script and spreadsheets):
| File | Role |
|---|---|
named_cells_rev1.json | Snapshot of named cells after the first revision’s extraction |
named_cells_rev2.json | Snapshot after the second revision’s extraction |
Verify in the UI
The script and the UI are not parallel demos: they act on the same files. Sign in to the same org you used for your token, then check:
- Files — Open the registered file (e.g.
Group3-UAS-Requirements) and match File ID and revision IDs to the script output. - Versions — Two revisions (baseline and updated spreadsheet), as in Platform 101.
- Activity or Jobs — Two Completed extractions for
open_spreadsheet/@istari:extract. - Resources — Extraction outputs including
named_cells.json, as in Platform 101 — Step 4. - Compare versions — Step 7 in Platform 101: use the in-app File Comparison; your terminal diff is the same story using the saved JSON files.
Note: Each list shows only the artifacts that that job produced — read from job.revision.products after the run — not everything that ever landed on the model. The script also saves a copy of the first named_cells to disk before the new spreadsheet is added, so the comparison stays valid even after the second extraction overwrites the model‑level artifact.
Understand the script
tutorial201.py first asserts that the sample spreadsheets and credentials are present.
The numbered steps then follow the same story as Platform 101:
connect → register → first extraction → save a snapshot of named_cells → add a new file revision → second extraction → save named_cells again → compare.
Connect
Configuration takes registry_url and registry_auth_token.
The script reads both from the environment (ISTARI_REGISTRY_URL and ISTARI_PERSONAL_ACCESS_TOKEN), so nothing secret lives in the code.
See the SDK Quick Start for defaults and other patterns.
A Personal Access Token is issued to a user: the client acts on your behalf—the same as when you are signed in—so register, list, and job calls use your authorization to read files, run tools and functions, and work only where your account is allowed. If something is denied in the API, the usual causes are the same as in the UI. See Sharing and access for how access to files and actions is decided.
Health check
client.readiness_check() is a fast round-trip that confirms the platform is reachable and your token is accepted.
Register the model and run the first extraction
add_modelregisters the spreadsheet and returns a model with a stable id.add_jobqueues@istari:extractforopen_spreadsheetand returns immediately with a job id and an initial status.wait_for_job(client, job)is a tiny helper defined at the top of the script: it callsclient.get_job(job.id)every 5 seconds, prints the status on each loop, and returns the refreshed job (withrevision.productspopulated) once it reachesCompleted. It raises if the job ends inFailedorCanceled. Depending on how fast the agent runs you'll see one or several of the states listed in theJobStatusNamereference.
The script then walks job1.revision.products once.
For each product, p.revision resolves to the exact FileRevision the agent wrote — the same revision_id and file_id you'd see in the UI — so we can print its name and ids and capture the one named named_cells.json.
Its bytes are saved to named_cells_rev1.json.
Add a new revision and run the second extraction
update_model adds a new revision to the same model — same id, new revision id.
A second add_job runs, wait_for_job polls until it completes (returning the refreshed job), and the script reads the new products the same way, writing the second named_cells artifact to named_cells_rev2.json.
Compare
As in Platform 101 — Compare versions, the goal is to see what changed between two revisions.
The extracted named_cells is JSON in a vendor‑neutral shape, so you are free to apply whatever custom logic fits the question — here a small dictionary name → value per revision, then a flat name: before → after line for any name where the value differs.
The same JSON could feed a chart, a filter on specific named ranges, or a short narrative drafted by an LLM.
The Summary block above the table prints the file name, model id, and the from and to revision ids (plus time and uploader when the client returns them). Whether the change description is written by code or by a model, it stays grounded in the immutable source of truth the Istari Digital Platform already recorded: the file and the specific revisions it came from.
What you learned
The same actions you took in the UI in Platform 101, now from Python — on the same files, with the same model and revision ids:
| Concept | Platform 101 (UI) | Python (this tutorial) |
|---|---|---|
| Register a file | Drag and drop | client.add_model(path=...) |
| Run an extraction | Create job in UI | client.add_job(...) then poll with client.get_job(job.id) |
| Monitor a job | Activity | client.get_job(job.id).status.name in a loop |
| See what a job produced | Resources tab on a Job | job.revision.products → client.get_artifact(p.resource_id) |
| Download a resource | Click Download | artifact.read_bytes() → file on disk |
| Upload a new version | Add version | client.update_model(model_id=..., path=...) |
| Custom logic on extracted data | Compare versions | Any Python you like (name: before → after list here; could be a chart, filter, LLM narrative…) on the JSON, anchored to file / model / revision ids in the platform |
Full script
The complete tutorial201.py for reference. You can also download it.
"""Tutorial 201: register a spreadsheet, extract data twice, compare named_cells JSON locally."""
import json
import os
import sys
import time
from pathlib import Path
from istari_digital_client import Client, Configuration
from istari_digital_client.exceptions import ApiException, ServiceException
from urllib3.exceptions import MaxRetryError
def _on_uncaught(exc_type, exc, tb):
if issubclass(exc_type, MaxRetryError):
print("\nCannot reach the Istari Digital Platform.")
print("Check ISTARI_REGISTRY_URL and your ISTARI_PERSONAL_ACCESS_TOKEN, then try again.")
sys.exit(1)
sys.__excepthook__(exc_type, exc, tb)
sys.excepthook = _on_uncaught
def wait_for_job(client, job, interval=5):
"""Poll a job until it reaches a terminal state, printing the status each loop.
Returns the refreshed Job; raises if it ends in Failed or Canceled.
Transient service errors (5xx) are retried silently."""
while True:
try:
job = client.get_job(job.id)
except ServiceException:
time.sleep(interval)
continue
label = getattr(job.status.name, "value", job.status.name)
print(f" status: {label}")
if label == "Completed":
return job
if label in ("Failed", "Canceled"):
raise RuntimeError(f"Job {job.id} ended in state {label}.")
time.sleep(interval)
baseline_path = Path("Group3-UAS-Requirements.xlsx")
revised_path = Path("Group3-UAS-Requirements-v2.xlsx")
assert baseline_path.is_file(), (
f"Expected {baseline_path.name!r} in {str(Path.cwd())!r}. "
"Download the sample from the Python Client 201 tutorial, save it next to this script, then run again."
)
assert revised_path.is_file(), (
f"Expected {revised_path.name!r} in {str(Path.cwd())!r}. "
"Download the v2 sample from the same tutorial, same folder, then run again."
)
registry_url = (os.environ.get("ISTARI_REGISTRY_URL") or "").strip()
token = (os.environ.get("ISTARI_PERSONAL_ACCESS_TOKEN") or "").strip()
assert registry_url, (
"ISTARI_REGISTRY_URL is missing or empty. In the platform: Settings → Developer Settings, "
"copy Registry URL. In this terminal: export ISTARI_REGISTRY_URL='https://...' "
"then run: python tutorial201.py"
)
assert token, (
"ISTARI_PERSONAL_ACCESS_TOKEN is missing or empty. Under Developer Settings, create or copy a token, then "
"in this terminal: export ISTARI_PERSONAL_ACCESS_TOKEN='...' and run: python tutorial201.py"
)
# 1. Connect
client = Client(
Configuration(
registry_url=registry_url,
registry_auth_token=token,
)
)
# 2. Health check — confirms the platform is reachable and your token is accepted.
try:
report = client.readiness_check() # or client.liveness_check() for a lighter probe
except (ApiException, MaxRetryError) as e:
print(f"\nCannot reach the Istari Digital Platform ({type(e).__name__}).")
print("Check ISTARI_REGISTRY_URL and your ISTARI_PERSONAL_ACCESS_TOKEN, then try again.")
sys.exit(1)
print(report)
if not report.healthy:
raise RuntimeError(f"Platform reports unhealthy: {report}")
# 3. Register the first spreadsheet (same as uploading in Files)
model = client.add_model(path=baseline_path)
first_rev = model.file.revisions[0]
print(f"Model registered: {first_rev.name}")
print(f" Model ID: {model.id}")
print(f" Revision ID: {first_rev.id}")
# 4. First extraction — Open Spreadsheet → @istari:extract
job1 = client.add_job(
model_id=model.id,
function="@istari:extract",
tool_name="open_spreadsheet",
)
print(f"\nJob: {job1.id}")
job1 = wait_for_job(client, job1)
print("First extraction done.")
# 5. Inspect what job 1 produced and capture the named_cells revision.
# Each Product points to the exact FileRevision the agent wrote (race-safe).
products_1 = job1.revision.products or []
print(f"\nResources produced by job 1 ({len(products_1)} items):")
named_cells_rev1 = None
for p in products_1:
rev = p.revision
print(f" - {rev.name} (file={rev.file_id} rev={rev.id})")
if rev.name and "named_cells" in rev.name:
named_cells_rev1 = rev
if named_cells_rev1 is None:
raise RuntimeError("No named_cells.json was produced by the first extraction job.")
path_named_cells_1 = Path("named_cells_rev1.json")
path_named_cells_1.write_bytes(named_cells_rev1.read_bytes())
print(f"\nSaved {path_named_cells_1.resolve()} (rev {named_cells_rev1.id})")
# 6. Add the second spreadsheet as a new version of the same file
model_with_2nd_rev = client.update_model(model_id=model.id, path=revised_path)
new_rev = model_with_2nd_rev.file.revisions[-1]
print("\nSecond file uploaded (new revision).")
print(f" Model ID: {model_with_2nd_rev.id} (same as before)")
print(f" Revision ID: {new_rev.id}")
# 7. Second extraction on the current revision
job2 = client.add_job(
model_id=model.id,
function="@istari:extract",
tool_name="open_spreadsheet",
)
print(f"\nJob: {job2.id}")
job2 = wait_for_job(client, job2)
print("Second extraction done.")
# 8. Same pattern for job 2
products_2 = job2.revision.products or []
print(f"\nResources produced by job 2 ({len(products_2)} items):")
named_cells_rev2 = None
for p in products_2:
rev = p.revision
print(f" - {rev.name} (file={rev.file_id} rev={rev.id})")
if rev.name and "named_cells" in rev.name:
named_cells_rev2 = rev
if named_cells_rev2 is None:
raise RuntimeError("No named_cells.json was produced by the second extraction job.")
path_named_cells_2 = Path("named_cells_rev2.json")
path_named_cells_2.write_bytes(named_cells_rev2.read_bytes())
print(f"\nSaved {path_named_cells_2.resolve()} (rev {named_cells_rev2.id})")
# 9. Compare: list named-cell value changes between the two extractions
cells_1 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_1.read_text(encoding="utf-8"))}
cells_2 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_2.read_text(encoding="utf-8"))}
changes = sorted(
(name, cells_1.get(name), cells_2.get(name))
for name in set(cells_1) | set(cells_2)
if cells_1.get(name) != cells_2.get(name)
)
print("\n--- Summary (file and revisions from the platform) ---\n")
print(f"Named parameters (named_cells) for {first_rev.name!r} — model {model.id}")
print(f" from: revision {first_rev.id} · {first_rev.created.isoformat(timespec='seconds')} · user {first_rev.created_by_id}")
print(f" to: revision {new_rev.id} · {new_rev.created.isoformat(timespec='seconds')} · user {new_rev.created_by_id}")
print(f"\nChange detail ({len(changes)} named cell(s) changed):")
if not changes:
print(" (no value changes)")
for name, before, after in changes:
print(f" - {name}: {before} → {after}")
What’s next
- Pipelines — Sequential pipeline
- Parameter studies — Parameter study
- API reference — Python client