Metadata-Version: 2.4
Name: spinorio
Version: 1.1.0
Summary: Spinorio Workflow, Weylattice compilation and Hilvarian optimization client
Author: Spinorio Inc
License: Proprietary
Requires-Python: <3.14,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.27
Requires-Dist: spinorio-runtime==1.1.0
Requires-Dist: ipython>=8.18
Requires-Dist: keyring<27,>=25
Requires-Dist: filelock<4,>=3.16
Provides-Extra: qiskit
Requires-Dist: qiskit<2.6,>=2.5; extra == "qiskit"
Dynamic: license-file

# Spinorio Python SDK

Workflow capture and retrieval, Weylattice compilation, and Hilvarian optimization from your own Python environment.

Install Spinorio 1.1.0:

```bash
python -m pip install 'spinorio[qiskit]==1.1.0'
```

`spinorio-runtime` is a **required dependency**, including when installing plain `spinorio`. The native sensor is installed by default. The `qiskit` extra installs the supported Qiskit 2.5 series for circuit helpers and workflow capture; it does not control sensor installation. Importing the package does not start capture.

Supported environments: CPython 3.12 and 3.13, Windows x64, Linux x64 with glibc, and macOS Intel/Apple Silicon. Python 3.14, PyPy, Windows ARM and Linux ARM are not declared supported in this release.

## Authentication

Create an expiring token at **Account → API tokens** on spinorio.com. Copy it when shown. You can list token metadata and revoke it later; the full secret cannot be retrieved again.

```python
import os
from spinorio import Spinorio

client = Spinorio(token=os.environ['SPINORIO_API_TOKEN'])
print(client.usage())
```

Inside Spinorio Lab, use the same client without copying a token:

```python
from spinorio import Spinorio
client = Spinorio()
```

The client reuses your authenticated Lab workspace. On your laptop, `Spinorio()` reads `SPINORIO_API_TOKEN` automatically. You can also use an operating-system credential store:

```bash
spinorio auth save
spinorio usage
```

The prompt hides your token. `spinorio --profile research auth save` and `Spinorio(profile='research')` select a named profile. Credential selection is explicit token, explicit named profile, environment token, authenticated Lab workspace, then default saved profile. An invalid explicit credential raises an error; the client never switches accounts silently. No browser login is required. On a headless system without a working keyring, pass the token or use the environment variable. Never place a real token in a shared notebook or Git repository.

Tokens authenticate to `https://api.spinorio.com/api/v1`; the client exchanges them for short-lived access tokens. Different client instances keep separate credentials. Revocation rejects subsequent requests, including requests with an unexpired access token. Accepted jobs continue after revocation. Plan changes are reflected without replacing the personal token; the entitlement cache lasts at most 30 seconds.

## Compile

```python
from qiskit import QuantumCircuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

print(client.compile.list_engines())          # ['weylattice']
print(client.compile.get_default_engine())    # 'weylattice'
print(client.compile.targets())

job = client.compile.run(qc, target_id='linear_5')
print(job.job_id)
print(job.status())
compiled = job.result(wait=True, timeout=360)
print(job.metrics())
job.download('compiled.qpy')

# Reconnect to existing work, including from another machine:
job = client.compile.get(job.job_id)

# Synchronous convenience:
compiled = client.qcompile(qc, backend='linear_5')
```

`linear_5` is a reference topology, not a physical QPU. Engine selection is optional: `engine='weylattice'` is the default. Unsupported engines are rejected. Fetch the catalog again to discover later additions.

For IBM targets, connect your provider account first, select an instance using `client.compile.ibm_instances()`, then select a returned target from `client.compile.ibm_backends(instance_id)`. Existing provider connections from Lab are reused. A token with `providers:manage` can call `client.connect_ibm(api_token)`, `client.provider_connection()` and `client.disconnect_ibm()`. Provider credentials are stored by the existing encrypted control-plane path and are never returned through the public API. An API subscription does not include provider QPU charges.

## Solve

```python
print(client.solvers.list_engines())          # ['hilvarian']
print(client.solvers.get_default_engine())    # 'hilvarian'
print(client.solvers.capabilities())

problem = {
    'schema': 'spinorio.solver.model.v1',
    'problem_type': 'maxcut',
    'variables': ['a', 'b', 'c'],
    'edges': [['a', 'b', 1], ['b', 'c', 1], ['c', 'a', 1]],
}
run = client.solvers.optimization.run(
    problem=problem, problem_type='maxcut', executor='local_numpy',
    engine='hilvarian',
)
print(run.run_id, run.status())
state = run.wait(timeout=300)
result = run.result()
print(run.events())
for item in run.artifacts():
    print(item)

same_run = client.solvers.get(run.run_id)
# same_run.cancel()
# same_run.resume(recalibrate=True)
# same_run.download(artifact_id, 'evidence.json')
```

`local_numpy` and `local_aer` are server-side executors. Availability comes from `capabilities()`. IBM execution retains the existing per-account hardware gate. No automatic escalation to paid hardware occurs. Search, error-management and budget parameters use the existing solver contract. Basic/Plus API admissions also enforce per-run wall-time and shot ceilings. `result()` reports an error when no result exists; `wait()` can also return a paused or failed state.

For uncertain submission acknowledgements, retain the exception and retry its exact request:

```python
from spinorio import SubmissionUncertain
try:
    run = client.solvers.run(model_id=saved_model_id, idempotency_key='my-experiment-001')
except SubmissionUncertain as error:
    run = error.retry()
```

The retry retains the originating client, model and idempotency key. Changing request settings with the same key returns a conflict. Ordinary reads, polls and idempotent job retries do not consume another job allowance. Keep your returned job/run IDs.

## Workflow in your existing notebook

```python
client.workflow.run()              # current IPython kernel only
# Run your quantum experiment in following cells.
client.workflow.status()           # sender, native observer and QPY queue health
client.workflow.flush(timeout=30)  # wait for pending uploads
client.workflow.stop()             # stop hooks; unsent work stays on disk
```

Spinorio Lab starts automatic capture when its kernel starts. Calling `client.workflow.run()` again with the same account and settings reuses that sensor. To change capture settings, call `client.workflow.stop()` first, then `client.workflow.run(...)`. On your laptop, capture starts only when you call `run()` or explicitly enable autostart.

Automatic mode uses the existing Qiskit observation adapters: circuits and versions, supported transpilation boundaries, provider jobs and already-returned results. It is not a recorder of every Python object, arbitrary file or complete kernel memory. A completed experiment cannot be reconstructed if it was never observed or its QPY capture failed. Use `status()` to inspect dropped, rejected, pending and failed delivery counts.

Selective mode is deliberately narrower: it takes supported named-object snapshots at cell boundaries and disables global runtime-boundary capture:

```python
client.workflow.run(
    mode='selective', objects=['qc', 'job', 'result'],
    kinds=['circuits', 'jobs', 'results'],
)
```

Names must exist in the active kernel when the observed cell finishes. Direct QuantumCircuit, supported job/result and pass-manager objects are eligible. Selective mode does not recursively capture arbitrary containers or full intermediate call histories. Raw cell code and exception messages are off by default; opt in with `include_code=True` or `include_errors=True`.

Queues are bounded and stored under `~/.spinorio/capture/`, partitioned by account, API origin and sensor session. They contain experiment data, not your API token. Delivery retries use persisted payloads. After a kernel crash:

```python
print(client.workflow.pending_sessions())
client.workflow.run(resume_session_id='sdk_...')  # choose a returned session ID
```

Only one kernel may use a given spool at a time. Retrying an unchanged payload does not double-charge ingress bytes. Queue overflow is reported rather than silently accepting data. Revocation or exhausted allowance can interrupt delivery, so preserve pending queues until the issue is resolved.

From a terminal:

```bash
spinorio workflow run                  # opens a new captured IPython session
spinorio workflow run -- experiment.py # executes a script in captured IPython
spinorio workflow enable-autostart     # explicitly enable future default-profile kernels
spinorio workflow disable-autostart
```

The CLI cannot attach to a different, already-running kernel. Use the Python call inside that kernel. Autostart is optional; installation and ordinary `run()` do not edit IPython startup files. For scripts, runtime boundaries are captured during the script; namespace snapshots occur at the script's end.

## Recall and analysis

```python
jobs = client.workflow.jobs.list(limit=50)
record = client.workflow.jobs.get(job_id)
analysis = client.workflow.jobs.metrics(job_id)
# client.workflow.jobs.refresh(job_id)  # refresh supported provider jobs using your stored connection
units = client.workflow.jobs.units(job_id, limit=50, offset=0)

circuits = client.workflow.circuits.list(limit=50)
versions = client.workflow.circuits.versions(circuit_id)
snapshot = client.workflow.circuits.version(circuit_version_id)
qc = client.workflow.circuits.load(circuit_version_id)
diagram_data = client.workflow.circuits.diagram(circuit_version_id)
print(client.workflow.circuits.draw(circuit_version_id, output='text'))
metrics = client.workflow.circuits.metrics(circuit_version_id)
```

`load()` returns a Qiskit circuit from stored QPY. `diagram()` returns the control plane's structured diagram data; `draw()` renders locally using Qiskit. Metrics are the control plane's available analyses, not invented values. Missing artifacts return a clear API error. Nothing unpickles arbitrary remote Python objects.

## Account usage

`client.usage()` returns your current plan, monthly limits and usage. Basic includes 10 compilation jobs and 10 solver runs per month; Plus includes 200 of each. Workflow capture is measured by uploaded data, with 10 MiB per month on Basic and 1 GiB on Plus. Reads and polling do not use another job allowance. Upload and solver execution limits are also returned by `usage()`.

Usage belongs to your account across Lab, laptops and API tokens. Creating another token does not reset it. Lab session time is a separate allowance. If an allowance is exhausted, the API returns HTTP 429; `APIError` exposes `status_code`, `detail` and `retry_after`. Check usage or upgrade through your account.
