spinorio 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spinorio-1.1.0/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) 2026 Spinorio Inc. All rights reserved.
2
+
3
+ Proprietary software. No open-source license is granted by this distribution.
4
+ Use is subject to the applicable Spinorio service terms.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: spinorio
3
+ Version: 1.1.0
4
+ Summary: Spinorio Workflow, Weylattice compilation and Hilvarian optimization client
5
+ Author: Spinorio Inc
6
+ License: Proprietary
7
+ Requires-Python: <3.14,>=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: httpx<1,>=0.27
11
+ Requires-Dist: spinorio-runtime==1.1.0
12
+ Requires-Dist: ipython>=8.18
13
+ Requires-Dist: keyring<27,>=25
14
+ Requires-Dist: filelock<4,>=3.16
15
+ Provides-Extra: qiskit
16
+ Requires-Dist: qiskit<2.6,>=2.5; extra == "qiskit"
17
+ Dynamic: license-file
18
+
19
+ # Spinorio Python SDK
20
+
21
+ Workflow capture and retrieval, Weylattice compilation, and Hilvarian optimization from your own Python environment.
22
+
23
+ Install Spinorio 1.1.0:
24
+
25
+ ```bash
26
+ python -m pip install 'spinorio[qiskit]==1.1.0'
27
+ ```
28
+
29
+ `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.
30
+
31
+ 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.
32
+
33
+ ## Authentication
34
+
35
+ 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.
36
+
37
+ ```python
38
+ import os
39
+ from spinorio import Spinorio
40
+
41
+ client = Spinorio(token=os.environ['SPINORIO_API_TOKEN'])
42
+ print(client.usage())
43
+ ```
44
+
45
+ Inside Spinorio Lab, use the same client without copying a token:
46
+
47
+ ```python
48
+ from spinorio import Spinorio
49
+ client = Spinorio()
50
+ ```
51
+
52
+ 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:
53
+
54
+ ```bash
55
+ spinorio auth save
56
+ spinorio usage
57
+ ```
58
+
59
+ 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.
60
+
61
+ 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.
62
+
63
+ ## Compile
64
+
65
+ ```python
66
+ from qiskit import QuantumCircuit
67
+
68
+ qc = QuantumCircuit(2)
69
+ qc.h(0)
70
+ qc.cx(0, 1)
71
+
72
+ print(client.compile.list_engines()) # ['weylattice']
73
+ print(client.compile.get_default_engine()) # 'weylattice'
74
+ print(client.compile.targets())
75
+
76
+ job = client.compile.run(qc, target_id='linear_5')
77
+ print(job.job_id)
78
+ print(job.status())
79
+ compiled = job.result(wait=True, timeout=360)
80
+ print(job.metrics())
81
+ job.download('compiled.qpy')
82
+
83
+ # Reconnect to existing work, including from another machine:
84
+ job = client.compile.get(job.job_id)
85
+
86
+ # Synchronous convenience:
87
+ compiled = client.qcompile(qc, backend='linear_5')
88
+ ```
89
+
90
+ `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.
91
+
92
+ 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.
93
+
94
+ ## Solve
95
+
96
+ ```python
97
+ print(client.solvers.list_engines()) # ['hilvarian']
98
+ print(client.solvers.get_default_engine()) # 'hilvarian'
99
+ print(client.solvers.capabilities())
100
+
101
+ problem = {
102
+ 'schema': 'spinorio.solver.model.v1',
103
+ 'problem_type': 'maxcut',
104
+ 'variables': ['a', 'b', 'c'],
105
+ 'edges': [['a', 'b', 1], ['b', 'c', 1], ['c', 'a', 1]],
106
+ }
107
+ run = client.solvers.optimization.run(
108
+ problem=problem, problem_type='maxcut', executor='local_numpy',
109
+ engine='hilvarian',
110
+ )
111
+ print(run.run_id, run.status())
112
+ state = run.wait(timeout=300)
113
+ result = run.result()
114
+ print(run.events())
115
+ for item in run.artifacts():
116
+ print(item)
117
+
118
+ same_run = client.solvers.get(run.run_id)
119
+ # same_run.cancel()
120
+ # same_run.resume(recalibrate=True)
121
+ # same_run.download(artifact_id, 'evidence.json')
122
+ ```
123
+
124
+ `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.
125
+
126
+ For uncertain submission acknowledgements, retain the exception and retry its exact request:
127
+
128
+ ```python
129
+ from spinorio import SubmissionUncertain
130
+ try:
131
+ run = client.solvers.run(model_id=saved_model_id, idempotency_key='my-experiment-001')
132
+ except SubmissionUncertain as error:
133
+ run = error.retry()
134
+ ```
135
+
136
+ 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.
137
+
138
+ ## Workflow in your existing notebook
139
+
140
+ ```python
141
+ client.workflow.run() # current IPython kernel only
142
+ # Run your quantum experiment in following cells.
143
+ client.workflow.status() # sender, native observer and QPY queue health
144
+ client.workflow.flush(timeout=30) # wait for pending uploads
145
+ client.workflow.stop() # stop hooks; unsent work stays on disk
146
+ ```
147
+
148
+ 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.
149
+
150
+ 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.
151
+
152
+ Selective mode is deliberately narrower: it takes supported named-object snapshots at cell boundaries and disables global runtime-boundary capture:
153
+
154
+ ```python
155
+ client.workflow.run(
156
+ mode='selective', objects=['qc', 'job', 'result'],
157
+ kinds=['circuits', 'jobs', 'results'],
158
+ )
159
+ ```
160
+
161
+ 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`.
162
+
163
+ 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:
164
+
165
+ ```python
166
+ print(client.workflow.pending_sessions())
167
+ client.workflow.run(resume_session_id='sdk_...') # choose a returned session ID
168
+ ```
169
+
170
+ 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.
171
+
172
+ From a terminal:
173
+
174
+ ```bash
175
+ spinorio workflow run # opens a new captured IPython session
176
+ spinorio workflow run -- experiment.py # executes a script in captured IPython
177
+ spinorio workflow enable-autostart # explicitly enable future default-profile kernels
178
+ spinorio workflow disable-autostart
179
+ ```
180
+
181
+ 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.
182
+
183
+ ## Recall and analysis
184
+
185
+ ```python
186
+ jobs = client.workflow.jobs.list(limit=50)
187
+ record = client.workflow.jobs.get(job_id)
188
+ analysis = client.workflow.jobs.metrics(job_id)
189
+ # client.workflow.jobs.refresh(job_id) # refresh supported provider jobs using your stored connection
190
+ units = client.workflow.jobs.units(job_id, limit=50, offset=0)
191
+
192
+ circuits = client.workflow.circuits.list(limit=50)
193
+ versions = client.workflow.circuits.versions(circuit_id)
194
+ snapshot = client.workflow.circuits.version(circuit_version_id)
195
+ qc = client.workflow.circuits.load(circuit_version_id)
196
+ diagram_data = client.workflow.circuits.diagram(circuit_version_id)
197
+ print(client.workflow.circuits.draw(circuit_version_id, output='text'))
198
+ metrics = client.workflow.circuits.metrics(circuit_version_id)
199
+ ```
200
+
201
+ `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.
202
+
203
+ ## Account usage
204
+
205
+ `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()`.
206
+
207
+ 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.
@@ -0,0 +1,189 @@
1
+ # Spinorio Python SDK
2
+
3
+ Workflow capture and retrieval, Weylattice compilation, and Hilvarian optimization from your own Python environment.
4
+
5
+ Install Spinorio 1.1.0:
6
+
7
+ ```bash
8
+ python -m pip install 'spinorio[qiskit]==1.1.0'
9
+ ```
10
+
11
+ `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.
12
+
13
+ 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.
14
+
15
+ ## Authentication
16
+
17
+ 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.
18
+
19
+ ```python
20
+ import os
21
+ from spinorio import Spinorio
22
+
23
+ client = Spinorio(token=os.environ['SPINORIO_API_TOKEN'])
24
+ print(client.usage())
25
+ ```
26
+
27
+ Inside Spinorio Lab, use the same client without copying a token:
28
+
29
+ ```python
30
+ from spinorio import Spinorio
31
+ client = Spinorio()
32
+ ```
33
+
34
+ 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:
35
+
36
+ ```bash
37
+ spinorio auth save
38
+ spinorio usage
39
+ ```
40
+
41
+ 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.
42
+
43
+ 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.
44
+
45
+ ## Compile
46
+
47
+ ```python
48
+ from qiskit import QuantumCircuit
49
+
50
+ qc = QuantumCircuit(2)
51
+ qc.h(0)
52
+ qc.cx(0, 1)
53
+
54
+ print(client.compile.list_engines()) # ['weylattice']
55
+ print(client.compile.get_default_engine()) # 'weylattice'
56
+ print(client.compile.targets())
57
+
58
+ job = client.compile.run(qc, target_id='linear_5')
59
+ print(job.job_id)
60
+ print(job.status())
61
+ compiled = job.result(wait=True, timeout=360)
62
+ print(job.metrics())
63
+ job.download('compiled.qpy')
64
+
65
+ # Reconnect to existing work, including from another machine:
66
+ job = client.compile.get(job.job_id)
67
+
68
+ # Synchronous convenience:
69
+ compiled = client.qcompile(qc, backend='linear_5')
70
+ ```
71
+
72
+ `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.
73
+
74
+ 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.
75
+
76
+ ## Solve
77
+
78
+ ```python
79
+ print(client.solvers.list_engines()) # ['hilvarian']
80
+ print(client.solvers.get_default_engine()) # 'hilvarian'
81
+ print(client.solvers.capabilities())
82
+
83
+ problem = {
84
+ 'schema': 'spinorio.solver.model.v1',
85
+ 'problem_type': 'maxcut',
86
+ 'variables': ['a', 'b', 'c'],
87
+ 'edges': [['a', 'b', 1], ['b', 'c', 1], ['c', 'a', 1]],
88
+ }
89
+ run = client.solvers.optimization.run(
90
+ problem=problem, problem_type='maxcut', executor='local_numpy',
91
+ engine='hilvarian',
92
+ )
93
+ print(run.run_id, run.status())
94
+ state = run.wait(timeout=300)
95
+ result = run.result()
96
+ print(run.events())
97
+ for item in run.artifacts():
98
+ print(item)
99
+
100
+ same_run = client.solvers.get(run.run_id)
101
+ # same_run.cancel()
102
+ # same_run.resume(recalibrate=True)
103
+ # same_run.download(artifact_id, 'evidence.json')
104
+ ```
105
+
106
+ `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.
107
+
108
+ For uncertain submission acknowledgements, retain the exception and retry its exact request:
109
+
110
+ ```python
111
+ from spinorio import SubmissionUncertain
112
+ try:
113
+ run = client.solvers.run(model_id=saved_model_id, idempotency_key='my-experiment-001')
114
+ except SubmissionUncertain as error:
115
+ run = error.retry()
116
+ ```
117
+
118
+ 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.
119
+
120
+ ## Workflow in your existing notebook
121
+
122
+ ```python
123
+ client.workflow.run() # current IPython kernel only
124
+ # Run your quantum experiment in following cells.
125
+ client.workflow.status() # sender, native observer and QPY queue health
126
+ client.workflow.flush(timeout=30) # wait for pending uploads
127
+ client.workflow.stop() # stop hooks; unsent work stays on disk
128
+ ```
129
+
130
+ 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.
131
+
132
+ 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.
133
+
134
+ Selective mode is deliberately narrower: it takes supported named-object snapshots at cell boundaries and disables global runtime-boundary capture:
135
+
136
+ ```python
137
+ client.workflow.run(
138
+ mode='selective', objects=['qc', 'job', 'result'],
139
+ kinds=['circuits', 'jobs', 'results'],
140
+ )
141
+ ```
142
+
143
+ 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`.
144
+
145
+ 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:
146
+
147
+ ```python
148
+ print(client.workflow.pending_sessions())
149
+ client.workflow.run(resume_session_id='sdk_...') # choose a returned session ID
150
+ ```
151
+
152
+ 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.
153
+
154
+ From a terminal:
155
+
156
+ ```bash
157
+ spinorio workflow run # opens a new captured IPython session
158
+ spinorio workflow run -- experiment.py # executes a script in captured IPython
159
+ spinorio workflow enable-autostart # explicitly enable future default-profile kernels
160
+ spinorio workflow disable-autostart
161
+ ```
162
+
163
+ 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.
164
+
165
+ ## Recall and analysis
166
+
167
+ ```python
168
+ jobs = client.workflow.jobs.list(limit=50)
169
+ record = client.workflow.jobs.get(job_id)
170
+ analysis = client.workflow.jobs.metrics(job_id)
171
+ # client.workflow.jobs.refresh(job_id) # refresh supported provider jobs using your stored connection
172
+ units = client.workflow.jobs.units(job_id, limit=50, offset=0)
173
+
174
+ circuits = client.workflow.circuits.list(limit=50)
175
+ versions = client.workflow.circuits.versions(circuit_id)
176
+ snapshot = client.workflow.circuits.version(circuit_version_id)
177
+ qc = client.workflow.circuits.load(circuit_version_id)
178
+ diagram_data = client.workflow.circuits.diagram(circuit_version_id)
179
+ print(client.workflow.circuits.draw(circuit_version_id, output='text'))
180
+ metrics = client.workflow.circuits.metrics(circuit_version_id)
181
+ ```
182
+
183
+ `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.
184
+
185
+ ## Account usage
186
+
187
+ `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()`.
188
+
189
+ 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.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+ [project]
5
+ name = "spinorio"
6
+ version = "1.1.0"
7
+ description = "Spinorio Workflow, Weylattice compilation and Hilvarian optimization client"
8
+ readme = "README.md"
9
+ requires-python = ">=3.12,<3.14"
10
+ license = {text = "Proprietary"}
11
+ authors = [{name = "Spinorio Inc"}]
12
+ dependencies = ["httpx>=0.27,<1", "spinorio-runtime==1.1.0", "ipython>=8.18", "keyring>=25,<27", "filelock>=3.16,<4"]
13
+ [project.optional-dependencies]
14
+ qiskit = ["qiskit>=2.5,<2.6"]
15
+ [project.scripts]
16
+ spinorio = "spinorio.cli:main"
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
19
+ [tool.pytest.ini_options]
20
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Spinorio: one client for Workflow, Compile and Solvers, locally and in Lab."""
2
+
3
+ from .client import Spinorio, APIError, SubmissionUncertain
4
+
5
+ __version__ = "1.1.0"
6
+ __all__ = ["Spinorio", "APIError", "SubmissionUncertain"]
@@ -0,0 +1,112 @@
1
+ """Credential resolution for local Python and authenticated Spinorio Lab kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+ from urllib.parse import urlsplit
9
+
10
+ PUBLIC_API = "https://api.spinorio.com/api/v1"
11
+
12
+
13
+ def _url(value, *, internal=False):
14
+ value = value.rstrip("/")
15
+ parsed = urlsplit(value)
16
+ cluster_http = (
17
+ internal and parsed.scheme == "http"
18
+ and (parsed.hostname or "").endswith(".svc.cluster.local")
19
+ )
20
+ if (parsed.scheme != "https" and not cluster_http) or not parsed.netloc or any(
21
+ (parsed.username, parsed.password, parsed.query, parsed.fragment)
22
+ ):
23
+ raise ValueError("Use an HTTPS API URL without credentials, query or fragment")
24
+ return value
25
+
26
+
27
+ def _profile_token(base_url, profile):
28
+ import keyring
29
+
30
+ try:
31
+ return keyring.get_password("spinorio:" + base_url, profile)
32
+ except keyring.errors.KeyringError:
33
+ return None
34
+
35
+
36
+ class PersonalCredential:
37
+ kind = "api-token"
38
+
39
+ def __init__(self, token):
40
+ if not isinstance(token, str) or not token.strip():
41
+ raise ValueError("The API token must be a nonempty string")
42
+ self._token = token.strip()
43
+
44
+ def exchange(self, client):
45
+ return client._checked(client._http.post(
46
+ client.base_url + "/auth/token",
47
+ headers={"Authorization": "Bearer " + self._token},
48
+ json={"session_id": client.session_id},
49
+ )).json()
50
+
51
+
52
+ class LabCredential:
53
+ kind = "lab"
54
+
55
+ def __init__(self, base, path):
56
+ self.base = base
57
+ self.path = path
58
+
59
+ def exchange(self, client):
60
+ from filelock import FileLock
61
+
62
+ # Multiple kernels share the workspace credential. Rotate atomically and
63
+ # always reread it, including after another kernel refreshed the session.
64
+ with FileLock(str(self.path) + ".sdk.lock", timeout=30):
65
+ try:
66
+ token = self.path.read_text(encoding="utf-8").strip()
67
+ except OSError as exc:
68
+ raise RuntimeError("Lab credentials are unavailable. Restart your Lab session.") from exc
69
+ if not token:
70
+ raise ValueError("Lab credentials are empty. Restart your Lab session.")
71
+ data = client._checked(client._http.post(
72
+ self.base + "/v1/auth/sdk-token",
73
+ headers={"Authorization": "Bearer " + token},
74
+ json={"session_id": client.session_id},
75
+ )).json()
76
+ refresh = data.pop("refresh_token")
77
+ temporary = None
78
+ try:
79
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=self.path.parent, delete=False) as out:
80
+ temporary = Path(out.name)
81
+ os.chmod(temporary, 0o600)
82
+ out.write(refresh)
83
+ temporary.replace(self.path)
84
+ finally:
85
+ if temporary is not None and temporary.exists():
86
+ temporary.unlink()
87
+ return data
88
+
89
+
90
+ def resolve(token, base_url, profile):
91
+ """Explicit token, explicit profile, environment token, Lab, default profile."""
92
+ public = _url(base_url or os.getenv("SPINORIO_API_BASE_URL") or PUBLIC_API)
93
+ if token is not None:
94
+ return public, PersonalCredential(token)
95
+ if profile is not None:
96
+ value = _profile_token(public, profile)
97
+ if not value:
98
+ raise ValueError(f"No API token saved for profile {profile!r} at this API URL")
99
+ return public, PersonalCredential(value)
100
+ if "SPINORIO_API_TOKEN" in os.environ:
101
+ return public, PersonalCredential(os.environ["SPINORIO_API_TOKEN"])
102
+ if os.getenv("SPINORIO_SDK_LAB_MODE") == "1" and base_url is None and not os.getenv("SPINORIO_API_BASE_URL"):
103
+ base = os.getenv("SPINORIO_CONTROL_PLANE_BASE_URL", "").rstrip("/")
104
+ path = os.getenv("SPINORIO_SENSOR_REFRESH_TOKEN_PATH", "")
105
+ if not base or not path:
106
+ raise ValueError("Lab connection settings are unavailable. Restart your Lab session.")
107
+ base = _url(base, internal=True)
108
+ return base + "/api/v1", LabCredential(base, Path(path))
109
+ value = _profile_token(public, "default")
110
+ if not value:
111
+ raise ValueError("Supply token=, set SPINORIO_API_TOKEN, or save a profile with spinorio auth save")
112
+ return public, PersonalCredential(value)
@@ -0,0 +1,78 @@
1
+ import argparse
2
+ import getpass
3
+ import sys
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(prog="spinorio")
8
+ parser.add_argument("--profile")
9
+ commands = parser.add_subparsers(dest="command", required=True)
10
+ auth = commands.add_parser("auth").add_subparsers(dest="action", required=True)
11
+ auth.add_parser("save")
12
+ auth.add_parser("remove")
13
+ commands.add_parser("usage")
14
+ commands.add_parser("engines")
15
+ wf = commands.add_parser("workflow").add_subparsers(dest="action", required=True)
16
+ run = wf.add_parser("run")
17
+ run.add_argument("script", nargs="?")
18
+ run.add_argument("args", nargs=argparse.REMAINDER)
19
+ wf.add_parser("enable-autostart")
20
+ wf.add_parser("disable-autostart")
21
+ args = parser.parse_args()
22
+ from . import Spinorio
23
+
24
+ if args.command == "auth":
25
+ import keyring, os
26
+
27
+ service = "spinorio:" + os.getenv(
28
+ "SPINORIO_API_BASE_URL", "https://api.spinorio.com/api/v1"
29
+ ).rstrip("/")
30
+ profile = args.profile or "default"
31
+ if args.action == "remove":
32
+ keyring.delete_password(service, profile)
33
+ else:
34
+ token = getpass.getpass("Spinorio API token (hidden): ")
35
+ with Spinorio(token) as client:
36
+ client.sensor_token()
37
+ keyring.set_password(service, profile, token)
38
+ print("Profile updated in the operating system credential store.")
39
+ return
40
+ with Spinorio(profile=args.profile) as client:
41
+ if args.command in {"usage", "engines"}:
42
+ import json
43
+
44
+ print(json.dumps(getattr(client, args.command)(), indent=2))
45
+ return
46
+ if args.action == "enable-autostart":
47
+ print(client.workflow.enable_autostart(profile=args.profile))
48
+ return
49
+ if args.action == "disable-autostart":
50
+ client.workflow.disable_autostart()
51
+ return
52
+ from IPython.terminal.interactiveshell import TerminalInteractiveShell
53
+
54
+ shell = TerminalInteractiveShell.instance()
55
+ client.workflow.run(ipython=shell)
56
+ try:
57
+ if args.script:
58
+ from pathlib import Path
59
+
60
+ sys.argv = [args.script] + args.args
61
+ result = shell.run_cell(
62
+ Path(args.script).read_text(encoding="utf-8"), store_history=True
63
+ )
64
+ client.workflow.flush()
65
+ if not result.success:
66
+ raise SystemExit(1)
67
+ else:
68
+ print(
69
+ "Spinorio capture is active in this IPython session. Existing kernels are unaffected."
70
+ )
71
+ shell.mainloop()
72
+ client.workflow.flush()
73
+ finally:
74
+ client.workflow.stop()
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()