subspacecomputing 0.1.4__tar.gz → 0.1.6__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.
Files changed (20) hide show
  1. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/LICENSE +1 -1
  2. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/MANIFEST.in +6 -0
  3. {subspacecomputing-0.1.4/subspacecomputing.egg-info → subspacecomputing-0.1.6}/PKG-INFO +88 -10
  4. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/README.md +85 -7
  5. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/pyproject.toml +2 -2
  6. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/setup.py +4 -4
  7. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing/__init__.py +1 -1
  8. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing/client.py +189 -1
  9. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6/subspacecomputing.egg-info}/PKG-INFO +88 -10
  10. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing.egg-info/SOURCES.txt +1 -4
  11. subspacecomputing-0.1.4/tests/test_client_integration.py +0 -320
  12. subspacecomputing-0.1.4/tests/test_client_unit.py +0 -380
  13. subspacecomputing-0.1.4/tests/test_pandas_integration.py +0 -41
  14. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/setup.cfg +0 -0
  15. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing/errors.py +0 -0
  16. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing/utils/__init__.py +0 -0
  17. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing/utils/pandas_integration.py +0 -0
  18. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing.egg-info/dependency_links.txt +0 -0
  19. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing.egg-info/requires.txt +0 -0
  20. {subspacecomputing-0.1.4 → subspacecomputing-0.1.6}/subspacecomputing.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Beausoft Inc.
3
+ Copyright (c) 2025 Subspace Computing Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -4,6 +4,12 @@ include README.md
4
4
  include LICENSE
5
5
  include pyproject.toml
6
6
  recursive-include subspacecomputing *.py
7
+ prune tests
8
+ prune doc
9
+ prune examples
10
+ recursive-exclude tests *
11
+ recursive-exclude doc *
12
+ recursive-exclude examples *
7
13
  recursive-exclude * __pycache__
8
14
  recursive-exclude * *.py[co]
9
15
  recursive-exclude * .git*
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subspacecomputing
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Python SDK for ASTRIA — Subspace Computing Engine
5
5
  Home-page: https://www.subspacecomputing.com/developer
6
- Author: Subspace Computing
7
- Author-email: Subspace Computing <contact@beausoft.ca>
6
+ Author: Subspace Computing Inc.
7
+ Author-email: "Subspace Computing Inc." <contact@subspacecomputing.com>
8
8
  Project-URL: Documentation, https://www.subspacecomputing.com/developer
9
9
  Project-URL: Homepage, https://www.subspacecomputing.com/developer
10
10
  Project-URL: Support, https://www.subspacecomputing.com/developer
@@ -26,9 +26,9 @@ Dynamic: home-page
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-python
28
28
 
29
- # Subspace Computing Engine - Python SDK
29
+ # ASTRIA Python SDK
30
30
 
31
- Python SDK for the Subspace Computing Engine API (by Beausoft).
31
+ Python SDK for ASTRIA, the Subspace Computing Engine.
32
32
 
33
33
  ## Installation
34
34
 
@@ -45,9 +45,6 @@ from subspacecomputing import ASTRIA
45
45
 
46
46
  # Initialize the client (defaults to production URL)
47
47
  client = ASTRIA(api_key='your-api-key-here')
48
-
49
- # For local testing or custom environments (optional)
50
- # client = ASTRIA(api_key='your-api-key-here', base_url='http://localhost:8000')
51
48
  ```
52
49
 
53
50
  ### Teams (`X-Team-Id`)
@@ -176,6 +173,87 @@ print(f"Total capital: {result['aggregations']['capital_total']}")
176
173
  print(f"Average: {result['aggregations']['moyenne_capital']}")
177
174
  ```
178
175
 
176
+ ### Run a stored model (`projection_id` + inputs)
177
+
178
+ Execute a projection already saved in Subspace. Load `run_inputs` once (cache in your app), build UI/batch from that descriptor, then call `run_model` many times with only the overrides. Astria runs the calculation; model structure (`formula`, dist type, `per`) is never overridable.
179
+
180
+ ```python
181
+ proj = client.load_model(projection_id) # get_projection + in-memory cache on this client
182
+ # Or: proj = client.get_projection(projection_id)
183
+
184
+ result = client.run_model(projection_id, inputs={"capital": 25000})
185
+ print(result["final_values"]["capital"])
186
+ ```
187
+
188
+ Input shapes by `run_inputs.kind`:
189
+
190
+ | `kind` | Example value in `inputs` |
191
+ |--------|---------------------------|
192
+ | `init` | `{"capital": 25000}` |
193
+ | `params` | `{"taux": {"min": 0.02, "max": 0.05}}` |
194
+ | `values` | `{"inflation": [0.01, 0.02, 0.03]}` |
195
+ | `table_ref` / `table` | `{"mortality": "<uuid>"}` or `{"grid": {...}}` |
196
+ | `run` | `{"scenarios": 100, "steps": 12}` |
197
+
198
+ Unknown input keys → API **400** `unknown_run_input`. `rerun_run(run_id)` re-executes a past **run snapshot**, not the live model.
199
+
200
+ Pattern for a dynamic form / batch loop:
201
+
202
+ ```python
203
+ proj = client.load_model(projection_id)
204
+ inputs = {}
205
+ for desc in proj.get("run_inputs") or []:
206
+ kind = desc.get("kind")
207
+ name = desc["name"]
208
+ if kind == "init":
209
+ inputs[name] = desc.get("default") # or from UI
210
+ elif kind == "params":
211
+ inputs[name] = dict(desc.get("default") or {})
212
+ # elif kind == "values": ...
213
+ result = client.run_model(projection_id, inputs=inputs)
214
+ ```
215
+
216
+ ### Runs: read, artifact, rerun, replay
217
+
218
+ Four distinct operations on persisted runs:
219
+
220
+ | Method | What it does |
221
+ |--------|----------------|
222
+ | `get_run(run_id)` | Read metadata, stored SP Model snapshot, and `result_summary` |
223
+ | `get_run_artifact(artifact_id)` | Read heavy results already computed (`final_values` / URL) |
224
+ | `rerun_run(run_id, spec_patch=None)` | Re-execute the run's snapshot in Astria; creates a **new** run |
225
+ | `replay_run(run_id, scenario_id)` | Reproduce **one** Monte Carlo scenario when seeds were stored |
226
+
227
+ ```python
228
+ # Cross-language analysis: app computed → Python reads original results
229
+ run = client.get_run(run_id)
230
+ print(run["result_summary"])
231
+ if run.get("artefact_id"):
232
+ artifact = client.get_run_artifact(run["artefact_id"])
233
+ print(artifact.get("final_values"))
234
+
235
+ # Re-execute the exact snapshot (validation) — new run, same projection
236
+ baseline = client.rerun_run(run_id)
237
+ print(baseline["run_id"], baseline["source_spec_hash"], baseline["spec_changed"])
238
+
239
+ # What-if: fork the snapshot with a JSON Merge Patch (source run unchanged)
240
+ what_if = client.rerun_run(
241
+ run_id,
242
+ spec_patch={"meta": {"seed": 42}},
243
+ )
244
+ print(what_if["executed_spec_hash"], what_if["final_values"])
245
+
246
+ # Exact MC scenario reproduction (requires meta.store_seeds_for_replay at creation)
247
+ replay = client.replay_run(run_id, scenario_id=0)
248
+ ```
249
+
250
+ **Notes**
251
+
252
+ - `rerun_run` does **not** load the projection's current `dsl_template`; it uses the run's stored `spec`.
253
+ - Requires `persist_mode=full` (headless minimal/metadata keys cannot create a new run).
254
+ - Exact stochastic reproduction of one scenario uses `replay_run` + stored seeds; a plain `rerun_run` of a Monte Carlo may draw new samples unless `meta.seed` was in the snapshot.
255
+ - Live `table_refs` may resolve to newer data than at original execution time.
256
+
179
257
  ### Validation
180
258
 
181
259
  ```python
@@ -259,7 +337,7 @@ Check out the full documentation at https://www.subspacecomputing.com/developer
259
337
 
260
338
  API reference is available at https://www.subspacecomputing.com/docs
261
339
 
262
- For support, reach out to contact@beausoft.ca
340
+ For support, reach out to contact@subspacecomputing.com
263
341
 
264
342
  ## License
265
343
 
@@ -267,4 +345,4 @@ MIT License. Check the LICENSE file for details.
267
345
 
268
346
  ## Copyright
269
347
 
270
- © 2025 Beausoft Inc. All Rights Reserved
348
+ © 2026 Subspace Computing Inc. All Rights Reserved
@@ -1,6 +1,6 @@
1
- # Subspace Computing Engine - Python SDK
1
+ # ASTRIA Python SDK
2
2
 
3
- Python SDK for the Subspace Computing Engine API (by Beausoft).
3
+ Python SDK for ASTRIA, the Subspace Computing Engine.
4
4
 
5
5
  ## Installation
6
6
 
@@ -17,9 +17,6 @@ from subspacecomputing import ASTRIA
17
17
 
18
18
  # Initialize the client (defaults to production URL)
19
19
  client = ASTRIA(api_key='your-api-key-here')
20
-
21
- # For local testing or custom environments (optional)
22
- # client = ASTRIA(api_key='your-api-key-here', base_url='http://localhost:8000')
23
20
  ```
24
21
 
25
22
  ### Teams (`X-Team-Id`)
@@ -148,6 +145,87 @@ print(f"Total capital: {result['aggregations']['capital_total']}")
148
145
  print(f"Average: {result['aggregations']['moyenne_capital']}")
149
146
  ```
150
147
 
148
+ ### Run a stored model (`projection_id` + inputs)
149
+
150
+ Execute a projection already saved in Subspace. Load `run_inputs` once (cache in your app), build UI/batch from that descriptor, then call `run_model` many times with only the overrides. Astria runs the calculation; model structure (`formula`, dist type, `per`) is never overridable.
151
+
152
+ ```python
153
+ proj = client.load_model(projection_id) # get_projection + in-memory cache on this client
154
+ # Or: proj = client.get_projection(projection_id)
155
+
156
+ result = client.run_model(projection_id, inputs={"capital": 25000})
157
+ print(result["final_values"]["capital"])
158
+ ```
159
+
160
+ Input shapes by `run_inputs.kind`:
161
+
162
+ | `kind` | Example value in `inputs` |
163
+ |--------|---------------------------|
164
+ | `init` | `{"capital": 25000}` |
165
+ | `params` | `{"taux": {"min": 0.02, "max": 0.05}}` |
166
+ | `values` | `{"inflation": [0.01, 0.02, 0.03]}` |
167
+ | `table_ref` / `table` | `{"mortality": "<uuid>"}` or `{"grid": {...}}` |
168
+ | `run` | `{"scenarios": 100, "steps": 12}` |
169
+
170
+ Unknown input keys → API **400** `unknown_run_input`. `rerun_run(run_id)` re-executes a past **run snapshot**, not the live model.
171
+
172
+ Pattern for a dynamic form / batch loop:
173
+
174
+ ```python
175
+ proj = client.load_model(projection_id)
176
+ inputs = {}
177
+ for desc in proj.get("run_inputs") or []:
178
+ kind = desc.get("kind")
179
+ name = desc["name"]
180
+ if kind == "init":
181
+ inputs[name] = desc.get("default") # or from UI
182
+ elif kind == "params":
183
+ inputs[name] = dict(desc.get("default") or {})
184
+ # elif kind == "values": ...
185
+ result = client.run_model(projection_id, inputs=inputs)
186
+ ```
187
+
188
+ ### Runs: read, artifact, rerun, replay
189
+
190
+ Four distinct operations on persisted runs:
191
+
192
+ | Method | What it does |
193
+ |--------|----------------|
194
+ | `get_run(run_id)` | Read metadata, stored SP Model snapshot, and `result_summary` |
195
+ | `get_run_artifact(artifact_id)` | Read heavy results already computed (`final_values` / URL) |
196
+ | `rerun_run(run_id, spec_patch=None)` | Re-execute the run's snapshot in Astria; creates a **new** run |
197
+ | `replay_run(run_id, scenario_id)` | Reproduce **one** Monte Carlo scenario when seeds were stored |
198
+
199
+ ```python
200
+ # Cross-language analysis: app computed → Python reads original results
201
+ run = client.get_run(run_id)
202
+ print(run["result_summary"])
203
+ if run.get("artefact_id"):
204
+ artifact = client.get_run_artifact(run["artefact_id"])
205
+ print(artifact.get("final_values"))
206
+
207
+ # Re-execute the exact snapshot (validation) — new run, same projection
208
+ baseline = client.rerun_run(run_id)
209
+ print(baseline["run_id"], baseline["source_spec_hash"], baseline["spec_changed"])
210
+
211
+ # What-if: fork the snapshot with a JSON Merge Patch (source run unchanged)
212
+ what_if = client.rerun_run(
213
+ run_id,
214
+ spec_patch={"meta": {"seed": 42}},
215
+ )
216
+ print(what_if["executed_spec_hash"], what_if["final_values"])
217
+
218
+ # Exact MC scenario reproduction (requires meta.store_seeds_for_replay at creation)
219
+ replay = client.replay_run(run_id, scenario_id=0)
220
+ ```
221
+
222
+ **Notes**
223
+
224
+ - `rerun_run` does **not** load the projection's current `dsl_template`; it uses the run's stored `spec`.
225
+ - Requires `persist_mode=full` (headless minimal/metadata keys cannot create a new run).
226
+ - Exact stochastic reproduction of one scenario uses `replay_run` + stored seeds; a plain `rerun_run` of a Monte Carlo may draw new samples unless `meta.seed` was in the snapshot.
227
+ - Live `table_refs` may resolve to newer data than at original execution time.
228
+
151
229
  ### Validation
152
230
 
153
231
  ```python
@@ -231,7 +309,7 @@ Check out the full documentation at https://www.subspacecomputing.com/developer
231
309
 
232
310
  API reference is available at https://www.subspacecomputing.com/docs
233
311
 
234
- For support, reach out to contact@beausoft.ca
312
+ For support, reach out to contact@subspacecomputing.com
235
313
 
236
314
  ## License
237
315
 
@@ -239,4 +317,4 @@ MIT License. Check the LICENSE file for details.
239
317
 
240
318
  ## Copyright
241
319
 
242
- © 2025 Beausoft Inc. All Rights Reserved
320
+ © 2026 Subspace Computing Inc. All Rights Reserved
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "subspacecomputing"
9
- version = "0.1.4"
9
+ version = "0.1.6"
10
10
  description = "Python SDK for ASTRIA — Subspace Computing Engine"
11
- authors = [{name = "Subspace Computing", email = "contact@beausoft.ca"}]
11
+ authors = [{name = "Subspace Computing Inc.", email = "contact@subspacecomputing.com"}]
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
14
14
  dependencies = [
@@ -9,16 +9,16 @@ try:
9
9
  with open("README.md", "r", encoding="utf-8") as fh:
10
10
  long_description = fh.read()
11
11
  except FileNotFoundError:
12
- long_description = "Python SDK for Subspace Computing Engine API (by Beausoft)"
12
+ long_description = "Python SDK for ASTRIA — Subspace Computing Engine API"
13
13
 
14
14
  setup(
15
15
  name="subspacecomputing",
16
- version="0.1.4",
16
+ version="0.1.6",
17
17
  description="Python SDK for ASTRIA — Subspace Computing Engine",
18
18
  long_description=long_description,
19
19
  long_description_content_type="text/markdown",
20
- author="Subspace Computing",
21
- author_email="contact@beausoft.ca",
20
+ author="Subspace Computing Inc.",
21
+ author_email="contact@subspacecomputing.com",
22
22
  url="https://www.subspacecomputing.com/developer",
23
23
  packages=find_packages(),
24
24
  install_requires=[
@@ -14,7 +14,7 @@ from .errors import (
14
14
  ValidationError,
15
15
  )
16
16
 
17
- __version__ = "0.1.4"
17
+ __version__ = "0.1.6"
18
18
  __all__ = [
19
19
  "ASTRIA",
20
20
  "SubspaceError",
@@ -6,6 +6,7 @@ This module provides a simple Python interface to interact with the Subspace Com
6
6
 
7
7
  import requests
8
8
  from typing import Dict, Any, Optional, List
9
+ from urllib.parse import quote
9
10
  from .errors import (
10
11
  SubspaceError,
11
12
  QuotaExceededError,
@@ -60,6 +61,7 @@ class ASTRIA:
60
61
  headers["X-Team-Id"] = self._team_id
61
62
  self.session.headers.update(headers)
62
63
  self.last_response = None # Store last response for header access
64
+ self._model_cache: Dict[str, Dict[str, Any]] = {}
63
65
 
64
66
  def set_team_id(self, team_id: Optional[str] = None) -> None:
65
67
  """Set or clear ``X-Team-Id`` for subsequent requests."""
@@ -148,6 +150,56 @@ class ASTRIA:
148
150
  self._handle_response(response)
149
151
  return response.json()
150
152
 
153
+ # Registry — execute published capability by name (production version)
154
+
155
+ def execute(
156
+ self,
157
+ name: str,
158
+ inputs: Optional[Dict[str, Any]] = None,
159
+ *,
160
+ correlation_id: Optional[str] = None,
161
+ ) -> Dict[str, Any]:
162
+ """
163
+ Execute a published Registry capability by name (resolves production version).
164
+
165
+ Args:
166
+ name: Capability name (e.g. ``calculatePremium``).
167
+ inputs: Parameters merged into the stored spec (batch_params / variables).
168
+ correlation_id: Optional trace id (body + ``X-Correlation-Id`` header).
169
+
170
+ Returns:
171
+ Simulate-style response plus ``capability``, ``capability_version``,
172
+ and optional ``correlation_id``.
173
+
174
+ Example:
175
+ result = client.execute(
176
+ "valorisation_ebitda",
177
+ inputs={"capital": 1_000_000},
178
+ correlation_id="quote-001",
179
+ )
180
+ """
181
+ if not name or not str(name).strip():
182
+ raise ValueError("Capability name is required.")
183
+
184
+ payload: Dict[str, Any] = {"inputs": inputs or {}}
185
+ if correlation_id:
186
+ payload["correlation_id"] = correlation_id
187
+
188
+ extra_headers: Dict[str, str] = {}
189
+ if correlation_id:
190
+ extra_headers["X-Correlation-Id"] = correlation_id
191
+
192
+ safe_name = quote(str(name).strip(), safe="")
193
+ response = self.session.post(
194
+ f"{self.base_url}/capabilities/{safe_name}/execute",
195
+ json=payload,
196
+ headers=extra_headers or None,
197
+ timeout=self.timeout,
198
+ )
199
+ self.last_response = response
200
+ self._handle_response(response)
201
+ return response.json()
202
+
151
203
  def replay_scenario(
152
204
  self,
153
205
  original_spec: Dict[str, Any],
@@ -328,7 +380,7 @@ class ASTRIA:
328
380
  projection_id: Projection ID
329
381
 
330
382
  Returns:
331
- Response with projection details
383
+ Response with projection details (includes ``run_inputs`` when present)
332
384
  """
333
385
  response = self.session.get(
334
386
  f"{self.base_url}/projections/{projection_id}",
@@ -338,6 +390,60 @@ class ASTRIA:
338
390
  self._handle_response(response)
339
391
  return response.json()
340
392
 
393
+ def load_model(self, projection_id: str) -> Dict[str, Any]:
394
+ """
395
+ Load a projection (schema + ``run_inputs``) with a simple in-memory cache
396
+ on this client instance. Prefer this once per session; still avoid calling
397
+ on every ``run_model``.
398
+ """
399
+ if not projection_id or not str(projection_id).strip():
400
+ raise ValueError("projection_id is required.")
401
+ pid = str(projection_id).strip()
402
+ cached = self._model_cache.get(pid)
403
+ if cached is not None:
404
+ return cached
405
+ proj = self.get_projection(pid)
406
+ self._model_cache[pid] = proj
407
+ return proj
408
+
409
+ def clear_model_cache(self, projection_id: Optional[str] = None) -> None:
410
+ """Clear in-memory model cache from :meth:`load_model`."""
411
+ if projection_id and str(projection_id).strip():
412
+ self._model_cache.pop(str(projection_id).strip(), None)
413
+ else:
414
+ self._model_cache.clear()
415
+
416
+ def run_model(
417
+ self,
418
+ projection_id: str,
419
+ inputs: Optional[Dict[str, Any]] = None,
420
+ ) -> Dict[str, Any]:
421
+ """
422
+ Run a stored projection by ID with optional runtime input overrides.
423
+
424
+ Applies ``run_inputs`` by kind (init, params, values, tables, run knobs).
425
+ Does not change formula / dist type / per. Distinct from :meth:`rerun_run`.
426
+
427
+ Args:
428
+ projection_id: Stored projection UUID
429
+ inputs: Optional overrides keyed by run_input name
430
+ (e.g. ``{"capital": 25000}``, ``{"taux": {"min": 0.02, "max": 0.05}}``)
431
+
432
+ Returns:
433
+ Same shape as ``project()`` or ``simulate()`` depending on scenarios
434
+ """
435
+ if not projection_id or not str(projection_id).strip():
436
+ raise ValueError("projection_id is required.")
437
+ safe_id = quote(str(projection_id).strip(), safe="")
438
+ response = self.session.post(
439
+ f"{self.base_url}/projections/{safe_id}/run",
440
+ json={"inputs": inputs or {}},
441
+ timeout=self.timeout,
442
+ )
443
+ self.last_response = response
444
+ self._handle_response(response)
445
+ return response.json()
446
+
341
447
  def delete_projection(self, projection_id: str) -> None:
342
448
  """
343
449
  Delete a projection.
@@ -414,6 +520,41 @@ class ASTRIA:
414
520
  self._handle_response(response)
415
521
  return response.json()
416
522
 
523
+ def get_run_proof(self, run_id: str, *, include_spec: bool = False) -> Dict[str, Any]:
524
+ """
525
+ Fetch Astria run proof pack for a projection run.
526
+
527
+ Args:
528
+ run_id: Projection run ID
529
+ include_spec: If True, include resolved SP snapshot in the proof
530
+
531
+ Returns:
532
+ Proof pack (schema_version, hashes, seed, …)
533
+ """
534
+ params = {"include_spec": "true"} if include_spec else None
535
+ response = self.session.get(
536
+ f"{self.base_url}/projection-runs/{run_id}/proof",
537
+ params=params,
538
+ timeout=self.timeout,
539
+ )
540
+ self.last_response = response
541
+ self._handle_response(response)
542
+ return response.json()
543
+
544
+ def get_execution_proof(self, execution_id: str) -> Dict[str, Any]:
545
+ """
546
+ Fetch Astria run proof pack from a Registry execution (lineage.astria).
547
+
548
+ Requires Console JWT auth on the session (same as other /executions routes).
549
+ """
550
+ response = self.session.get(
551
+ f"{self.base_url}/executions/{execution_id}/proof",
552
+ timeout=self.timeout,
553
+ )
554
+ self.last_response = response
555
+ self._handle_response(response)
556
+ return response.json()
557
+
417
558
  def replay_run(self, run_id: str, scenario_id: int) -> Dict[str, Any]:
418
559
  """
419
560
  Replay a scenario from a stored run (requires persisted seeds).
@@ -442,6 +583,53 @@ class ASTRIA:
442
583
  self._handle_response(response)
443
584
  return response.json()
444
585
 
586
+ def rerun_run(
587
+ self,
588
+ run_id: str,
589
+ spec_patch: Optional[Dict[str, Any]] = None,
590
+ ) -> Dict[str, Any]:
591
+ """
592
+ Re-execute the SP Model snapshot of a persisted run (optional what-if patch).
593
+
594
+ Creates a **new** run on the same projection with ``source_run_id`` set.
595
+ Does **not** mutate the source run or the projection's current template.
596
+
597
+ Unlike ``replay_run``, this recalculates the full snapshot (project if
598
+ scenarios==1, otherwise simulate). Exact Monte Carlo scenario reproduction
599
+ still requires ``replay_run`` with stored seeds.
600
+
601
+ Args:
602
+ run_id: Source run UUID.
603
+ spec_patch: Optional JSON Merge Patch applied to the stored spec
604
+ (objects merge recursively; arrays/scalars replace; null deletes).
605
+ Reserved fields ``run_id``, ``meta.projection_id``,
606
+ ``meta._replay_seeds`` are rejected by the API.
607
+
608
+ Returns:
609
+ Project or simulate response plus ``source_run_id``,
610
+ ``source_spec_hash``, ``executed_spec_hash``, ``spec_changed``.
611
+
612
+ Example:
613
+ baseline = client.rerun_run(run_id)
614
+ what_if = client.rerun_run(
615
+ run_id,
616
+ spec_patch={"meta": {"seed": 42}},
617
+ )
618
+ """
619
+ if not run_id or not str(run_id).strip():
620
+ raise ValueError("run_id is required.")
621
+ payload: Dict[str, Any] = {}
622
+ if spec_patch is not None:
623
+ payload["spec_patch"] = spec_patch
624
+ response = self.session.post(
625
+ f"{self.base_url}/projection-runs/{str(run_id).strip()}/rerun",
626
+ json=payload,
627
+ timeout=self.timeout,
628
+ )
629
+ self.last_response = response
630
+ self._handle_response(response)
631
+ return response.json()
632
+
445
633
  def delete_run(self, run_id: str) -> None:
446
634
  """
447
635
  Delete a run.