subspacecomputing 0.1.4__tar.gz → 0.1.5__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.
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/LICENSE +1 -1
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/MANIFEST.in +6 -0
- {subspacecomputing-0.1.4/subspacecomputing.egg-info → subspacecomputing-0.1.5}/PKG-INFO +48 -7
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/README.md +45 -4
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/pyproject.toml +2 -2
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/setup.py +4 -4
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/__init__.py +1 -1
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/client.py +98 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5/subspacecomputing.egg-info}/PKG-INFO +48 -7
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/SOURCES.txt +1 -4
- subspacecomputing-0.1.4/tests/test_client_integration.py +0 -320
- subspacecomputing-0.1.4/tests/test_client_unit.py +0 -380
- subspacecomputing-0.1.4/tests/test_pandas_integration.py +0 -41
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/setup.cfg +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/errors.py +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/utils/__init__.py +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/utils/pandas_integration.py +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/dependency_links.txt +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/requires.txt +0 -0
- {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
3
|
+
Version: 0.1.5
|
|
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@
|
|
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
|
-
#
|
|
29
|
+
# ASTRIA — Python SDK
|
|
30
30
|
|
|
31
|
-
Python SDK for the Subspace Computing Engine
|
|
31
|
+
Python SDK for ASTRIA, the Subspace Computing Engine.
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
34
34
|
|
|
@@ -176,6 +176,47 @@ print(f"Total capital: {result['aggregations']['capital_total']}")
|
|
|
176
176
|
print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### Runs: read, artifact, rerun, replay
|
|
180
|
+
|
|
181
|
+
Four distinct operations on persisted runs:
|
|
182
|
+
|
|
183
|
+
| Method | What it does |
|
|
184
|
+
|--------|----------------|
|
|
185
|
+
| `get_run(run_id)` | Read metadata, stored SP Model snapshot, and `result_summary` |
|
|
186
|
+
| `get_run_artifact(artifact_id)` | Read heavy results already computed (`final_values` / URL) |
|
|
187
|
+
| `rerun_run(run_id, spec_patch=None)` | Re-execute the run's snapshot in Astria; creates a **new** run |
|
|
188
|
+
| `replay_run(run_id, scenario_id)` | Reproduce **one** Monte Carlo scenario when seeds were stored |
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# Cross-language analysis: app computed → Python reads original results
|
|
192
|
+
run = client.get_run(run_id)
|
|
193
|
+
print(run["result_summary"])
|
|
194
|
+
if run.get("artefact_id"):
|
|
195
|
+
artifact = client.get_run_artifact(run["artefact_id"])
|
|
196
|
+
print(artifact.get("final_values"))
|
|
197
|
+
|
|
198
|
+
# Re-execute the exact snapshot (validation) — new run, same projection
|
|
199
|
+
baseline = client.rerun_run(run_id)
|
|
200
|
+
print(baseline["run_id"], baseline["source_spec_hash"], baseline["spec_changed"])
|
|
201
|
+
|
|
202
|
+
# What-if: fork the snapshot with a JSON Merge Patch (source run unchanged)
|
|
203
|
+
what_if = client.rerun_run(
|
|
204
|
+
run_id,
|
|
205
|
+
spec_patch={"meta": {"seed": 42}},
|
|
206
|
+
)
|
|
207
|
+
print(what_if["executed_spec_hash"], what_if["final_values"])
|
|
208
|
+
|
|
209
|
+
# Exact MC scenario reproduction (requires meta.store_seeds_for_replay at creation)
|
|
210
|
+
replay = client.replay_run(run_id, scenario_id=0)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Notes**
|
|
214
|
+
|
|
215
|
+
- `rerun_run` does **not** load the projection's current `dsl_template`; it uses the run's stored `spec`.
|
|
216
|
+
- Requires `persist_mode=full` (headless minimal/metadata keys cannot create a new run).
|
|
217
|
+
- 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.
|
|
218
|
+
- Live `table_refs` may resolve to newer data than at original execution time.
|
|
219
|
+
|
|
179
220
|
### Validation
|
|
180
221
|
|
|
181
222
|
```python
|
|
@@ -259,7 +300,7 @@ Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
|
259
300
|
|
|
260
301
|
API reference is available at https://www.subspacecomputing.com/docs
|
|
261
302
|
|
|
262
|
-
For support, reach out to contact@
|
|
303
|
+
For support, reach out to contact@subspacecomputing.com
|
|
263
304
|
|
|
264
305
|
## License
|
|
265
306
|
|
|
@@ -267,4 +308,4 @@ MIT License. Check the LICENSE file for details.
|
|
|
267
308
|
|
|
268
309
|
## Copyright
|
|
269
310
|
|
|
270
|
-
©
|
|
311
|
+
© 2026 Subspace Computing Inc. All Rights Reserved
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ASTRIA — Python SDK
|
|
2
2
|
|
|
3
|
-
Python SDK for the Subspace Computing Engine
|
|
3
|
+
Python SDK for ASTRIA, the Subspace Computing Engine.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -148,6 +148,47 @@ print(f"Total capital: {result['aggregations']['capital_total']}")
|
|
|
148
148
|
print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
### Runs: read, artifact, rerun, replay
|
|
152
|
+
|
|
153
|
+
Four distinct operations on persisted runs:
|
|
154
|
+
|
|
155
|
+
| Method | What it does |
|
|
156
|
+
|--------|----------------|
|
|
157
|
+
| `get_run(run_id)` | Read metadata, stored SP Model snapshot, and `result_summary` |
|
|
158
|
+
| `get_run_artifact(artifact_id)` | Read heavy results already computed (`final_values` / URL) |
|
|
159
|
+
| `rerun_run(run_id, spec_patch=None)` | Re-execute the run's snapshot in Astria; creates a **new** run |
|
|
160
|
+
| `replay_run(run_id, scenario_id)` | Reproduce **one** Monte Carlo scenario when seeds were stored |
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Cross-language analysis: app computed → Python reads original results
|
|
164
|
+
run = client.get_run(run_id)
|
|
165
|
+
print(run["result_summary"])
|
|
166
|
+
if run.get("artefact_id"):
|
|
167
|
+
artifact = client.get_run_artifact(run["artefact_id"])
|
|
168
|
+
print(artifact.get("final_values"))
|
|
169
|
+
|
|
170
|
+
# Re-execute the exact snapshot (validation) — new run, same projection
|
|
171
|
+
baseline = client.rerun_run(run_id)
|
|
172
|
+
print(baseline["run_id"], baseline["source_spec_hash"], baseline["spec_changed"])
|
|
173
|
+
|
|
174
|
+
# What-if: fork the snapshot with a JSON Merge Patch (source run unchanged)
|
|
175
|
+
what_if = client.rerun_run(
|
|
176
|
+
run_id,
|
|
177
|
+
spec_patch={"meta": {"seed": 42}},
|
|
178
|
+
)
|
|
179
|
+
print(what_if["executed_spec_hash"], what_if["final_values"])
|
|
180
|
+
|
|
181
|
+
# Exact MC scenario reproduction (requires meta.store_seeds_for_replay at creation)
|
|
182
|
+
replay = client.replay_run(run_id, scenario_id=0)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Notes**
|
|
186
|
+
|
|
187
|
+
- `rerun_run` does **not** load the projection's current `dsl_template`; it uses the run's stored `spec`.
|
|
188
|
+
- Requires `persist_mode=full` (headless minimal/metadata keys cannot create a new run).
|
|
189
|
+
- 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.
|
|
190
|
+
- Live `table_refs` may resolve to newer data than at original execution time.
|
|
191
|
+
|
|
151
192
|
### Validation
|
|
152
193
|
|
|
153
194
|
```python
|
|
@@ -231,7 +272,7 @@ Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
|
231
272
|
|
|
232
273
|
API reference is available at https://www.subspacecomputing.com/docs
|
|
233
274
|
|
|
234
|
-
For support, reach out to contact@
|
|
275
|
+
For support, reach out to contact@subspacecomputing.com
|
|
235
276
|
|
|
236
277
|
## License
|
|
237
278
|
|
|
@@ -239,4 +280,4 @@ MIT License. Check the LICENSE file for details.
|
|
|
239
280
|
|
|
240
281
|
## Copyright
|
|
241
282
|
|
|
242
|
-
©
|
|
283
|
+
© 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.
|
|
9
|
+
version = "0.1.5"
|
|
10
10
|
description = "Python SDK for ASTRIA — Subspace Computing Engine"
|
|
11
|
-
authors = [{name = "Subspace Computing", email = "contact@
|
|
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
|
|
12
|
+
long_description = "Python SDK for ASTRIA — Subspace Computing Engine API"
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="subspacecomputing",
|
|
16
|
-
version="0.1.
|
|
16
|
+
version="0.1.5",
|
|
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@
|
|
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=[
|
|
@@ -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,
|
|
@@ -148,6 +149,56 @@ class ASTRIA:
|
|
|
148
149
|
self._handle_response(response)
|
|
149
150
|
return response.json()
|
|
150
151
|
|
|
152
|
+
# Registry — execute published capability by name (production version)
|
|
153
|
+
|
|
154
|
+
def execute(
|
|
155
|
+
self,
|
|
156
|
+
name: str,
|
|
157
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
158
|
+
*,
|
|
159
|
+
correlation_id: Optional[str] = None,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Execute a published Registry capability by name (resolves production version).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: Capability name (e.g. ``calculatePremium``).
|
|
166
|
+
inputs: Parameters merged into the stored spec (batch_params / variables).
|
|
167
|
+
correlation_id: Optional trace id (body + ``X-Correlation-Id`` header).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Simulate-style response plus ``capability``, ``capability_version``,
|
|
171
|
+
and optional ``correlation_id``.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
result = client.execute(
|
|
175
|
+
"valorisation_ebitda",
|
|
176
|
+
inputs={"capital": 1_000_000},
|
|
177
|
+
correlation_id="quote-001",
|
|
178
|
+
)
|
|
179
|
+
"""
|
|
180
|
+
if not name or not str(name).strip():
|
|
181
|
+
raise ValueError("Capability name is required.")
|
|
182
|
+
|
|
183
|
+
payload: Dict[str, Any] = {"inputs": inputs or {}}
|
|
184
|
+
if correlation_id:
|
|
185
|
+
payload["correlation_id"] = correlation_id
|
|
186
|
+
|
|
187
|
+
extra_headers: Dict[str, str] = {}
|
|
188
|
+
if correlation_id:
|
|
189
|
+
extra_headers["X-Correlation-Id"] = correlation_id
|
|
190
|
+
|
|
191
|
+
safe_name = quote(str(name).strip(), safe="")
|
|
192
|
+
response = self.session.post(
|
|
193
|
+
f"{self.base_url}/capabilities/{safe_name}/execute",
|
|
194
|
+
json=payload,
|
|
195
|
+
headers=extra_headers or None,
|
|
196
|
+
timeout=self.timeout,
|
|
197
|
+
)
|
|
198
|
+
self.last_response = response
|
|
199
|
+
self._handle_response(response)
|
|
200
|
+
return response.json()
|
|
201
|
+
|
|
151
202
|
def replay_scenario(
|
|
152
203
|
self,
|
|
153
204
|
original_spec: Dict[str, Any],
|
|
@@ -442,6 +493,53 @@ class ASTRIA:
|
|
|
442
493
|
self._handle_response(response)
|
|
443
494
|
return response.json()
|
|
444
495
|
|
|
496
|
+
def rerun_run(
|
|
497
|
+
self,
|
|
498
|
+
run_id: str,
|
|
499
|
+
spec_patch: Optional[Dict[str, Any]] = None,
|
|
500
|
+
) -> Dict[str, Any]:
|
|
501
|
+
"""
|
|
502
|
+
Re-execute the SP Model snapshot of a persisted run (optional what-if patch).
|
|
503
|
+
|
|
504
|
+
Creates a **new** run on the same projection with ``source_run_id`` set.
|
|
505
|
+
Does **not** mutate the source run or the projection's current template.
|
|
506
|
+
|
|
507
|
+
Unlike ``replay_run``, this recalculates the full snapshot (project if
|
|
508
|
+
scenarios==1, otherwise simulate). Exact Monte Carlo scenario reproduction
|
|
509
|
+
still requires ``replay_run`` with stored seeds.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
run_id: Source run UUID.
|
|
513
|
+
spec_patch: Optional JSON Merge Patch applied to the stored spec
|
|
514
|
+
(objects merge recursively; arrays/scalars replace; null deletes).
|
|
515
|
+
Reserved fields ``run_id``, ``meta.projection_id``,
|
|
516
|
+
``meta._replay_seeds`` are rejected by the API.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Project or simulate response plus ``source_run_id``,
|
|
520
|
+
``source_spec_hash``, ``executed_spec_hash``, ``spec_changed``.
|
|
521
|
+
|
|
522
|
+
Example:
|
|
523
|
+
baseline = client.rerun_run(run_id)
|
|
524
|
+
what_if = client.rerun_run(
|
|
525
|
+
run_id,
|
|
526
|
+
spec_patch={"meta": {"seed": 42}},
|
|
527
|
+
)
|
|
528
|
+
"""
|
|
529
|
+
if not run_id or not str(run_id).strip():
|
|
530
|
+
raise ValueError("run_id is required.")
|
|
531
|
+
payload: Dict[str, Any] = {}
|
|
532
|
+
if spec_patch is not None:
|
|
533
|
+
payload["spec_patch"] = spec_patch
|
|
534
|
+
response = self.session.post(
|
|
535
|
+
f"{self.base_url}/projection-runs/{str(run_id).strip()}/rerun",
|
|
536
|
+
json=payload,
|
|
537
|
+
timeout=self.timeout,
|
|
538
|
+
)
|
|
539
|
+
self.last_response = response
|
|
540
|
+
self._handle_response(response)
|
|
541
|
+
return response.json()
|
|
542
|
+
|
|
445
543
|
def delete_run(self, run_id: str) -> None:
|
|
446
544
|
"""
|
|
447
545
|
Delete a run.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subspacecomputing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
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@
|
|
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
|
-
#
|
|
29
|
+
# ASTRIA — Python SDK
|
|
30
30
|
|
|
31
|
-
Python SDK for the Subspace Computing Engine
|
|
31
|
+
Python SDK for ASTRIA, the Subspace Computing Engine.
|
|
32
32
|
|
|
33
33
|
## Installation
|
|
34
34
|
|
|
@@ -176,6 +176,47 @@ print(f"Total capital: {result['aggregations']['capital_total']}")
|
|
|
176
176
|
print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### Runs: read, artifact, rerun, replay
|
|
180
|
+
|
|
181
|
+
Four distinct operations on persisted runs:
|
|
182
|
+
|
|
183
|
+
| Method | What it does |
|
|
184
|
+
|--------|----------------|
|
|
185
|
+
| `get_run(run_id)` | Read metadata, stored SP Model snapshot, and `result_summary` |
|
|
186
|
+
| `get_run_artifact(artifact_id)` | Read heavy results already computed (`final_values` / URL) |
|
|
187
|
+
| `rerun_run(run_id, spec_patch=None)` | Re-execute the run's snapshot in Astria; creates a **new** run |
|
|
188
|
+
| `replay_run(run_id, scenario_id)` | Reproduce **one** Monte Carlo scenario when seeds were stored |
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# Cross-language analysis: app computed → Python reads original results
|
|
192
|
+
run = client.get_run(run_id)
|
|
193
|
+
print(run["result_summary"])
|
|
194
|
+
if run.get("artefact_id"):
|
|
195
|
+
artifact = client.get_run_artifact(run["artefact_id"])
|
|
196
|
+
print(artifact.get("final_values"))
|
|
197
|
+
|
|
198
|
+
# Re-execute the exact snapshot (validation) — new run, same projection
|
|
199
|
+
baseline = client.rerun_run(run_id)
|
|
200
|
+
print(baseline["run_id"], baseline["source_spec_hash"], baseline["spec_changed"])
|
|
201
|
+
|
|
202
|
+
# What-if: fork the snapshot with a JSON Merge Patch (source run unchanged)
|
|
203
|
+
what_if = client.rerun_run(
|
|
204
|
+
run_id,
|
|
205
|
+
spec_patch={"meta": {"seed": 42}},
|
|
206
|
+
)
|
|
207
|
+
print(what_if["executed_spec_hash"], what_if["final_values"])
|
|
208
|
+
|
|
209
|
+
# Exact MC scenario reproduction (requires meta.store_seeds_for_replay at creation)
|
|
210
|
+
replay = client.replay_run(run_id, scenario_id=0)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Notes**
|
|
214
|
+
|
|
215
|
+
- `rerun_run` does **not** load the projection's current `dsl_template`; it uses the run's stored `spec`.
|
|
216
|
+
- Requires `persist_mode=full` (headless minimal/metadata keys cannot create a new run).
|
|
217
|
+
- 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.
|
|
218
|
+
- Live `table_refs` may resolve to newer data than at original execution time.
|
|
219
|
+
|
|
179
220
|
### Validation
|
|
180
221
|
|
|
181
222
|
```python
|
|
@@ -259,7 +300,7 @@ Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
|
259
300
|
|
|
260
301
|
API reference is available at https://www.subspacecomputing.com/docs
|
|
261
302
|
|
|
262
|
-
For support, reach out to contact@
|
|
303
|
+
For support, reach out to contact@subspacecomputing.com
|
|
263
304
|
|
|
264
305
|
## License
|
|
265
306
|
|
|
@@ -267,4 +308,4 @@ MIT License. Check the LICENSE file for details.
|
|
|
267
308
|
|
|
268
309
|
## Copyright
|
|
269
310
|
|
|
270
|
-
©
|
|
311
|
+
© 2026 Subspace Computing Inc. All Rights Reserved
|
|
@@ -12,7 +12,4 @@ subspacecomputing.egg-info/dependency_links.txt
|
|
|
12
12
|
subspacecomputing.egg-info/requires.txt
|
|
13
13
|
subspacecomputing.egg-info/top_level.txt
|
|
14
14
|
subspacecomputing/utils/__init__.py
|
|
15
|
-
subspacecomputing/utils/pandas_integration.py
|
|
16
|
-
tests/test_client_integration.py
|
|
17
|
-
tests/test_client_unit.py
|
|
18
|
-
tests/test_pandas_integration.py
|
|
15
|
+
subspacecomputing/utils/pandas_integration.py
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests d'intégration pour le SDK Python.
|
|
3
|
-
|
|
4
|
-
Ces tests réutilisent les SP Models des tests non-régression
|
|
5
|
-
et comparent les résultats SDK vs API directe.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from unittest.mock import Mock
|
|
12
|
-
|
|
13
|
-
# Ajouter le répertoire parent pour importer l'API
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
15
|
-
|
|
16
|
-
from fastapi.testclient import TestClient
|
|
17
|
-
from api.main import app
|
|
18
|
-
from subspacecomputing import ASTRIA
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.fixture
|
|
22
|
-
def api_client():
|
|
23
|
-
"""Client API FastAPI pour tests."""
|
|
24
|
-
return TestClient(app)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@pytest.fixture
|
|
28
|
-
def sdk_client(api_key, api_client):
|
|
29
|
-
"""
|
|
30
|
-
Client SDK pour tests, utilisant TestClient au lieu de requests.
|
|
31
|
-
|
|
32
|
-
Cette fixture crée un wrapper qui adapte TestClient pour qu'il
|
|
33
|
-
fonctionne avec le SDK qui utilise requests.Session.
|
|
34
|
-
"""
|
|
35
|
-
# Créer une instance du SDK
|
|
36
|
-
client = ASTRIA(api_key=api_key, base_url="http://testserver")
|
|
37
|
-
|
|
38
|
-
# Créer un mock pour requests.Session qui utilise TestClient
|
|
39
|
-
def make_request(method, url, **kwargs):
|
|
40
|
-
"""Adapter qui convertit requests vers TestClient."""
|
|
41
|
-
path = url.replace("http://testserver", "").replace("https://testserver", "")
|
|
42
|
-
if not path.startswith("/"):
|
|
43
|
-
path = "/" + path
|
|
44
|
-
headers = dict(client.session.headers)
|
|
45
|
-
headers.update(kwargs.get("headers", {}))
|
|
46
|
-
if method.upper() == "GET":
|
|
47
|
-
response = api_client.get(path, headers=headers)
|
|
48
|
-
elif method.upper() == "POST":
|
|
49
|
-
response = api_client.post(path, json=kwargs.get("json"), headers=headers)
|
|
50
|
-
elif method.upper() == "DELETE":
|
|
51
|
-
response = api_client.delete(path, headers=headers)
|
|
52
|
-
else:
|
|
53
|
-
raise ValueError(f"Méthode HTTP non supportée: {method}")
|
|
54
|
-
|
|
55
|
-
# Créer un mock Response qui ressemble à requests.Response
|
|
56
|
-
mock_response = Mock()
|
|
57
|
-
mock_response.status_code = response.status_code
|
|
58
|
-
mock_response.json = lambda: response.json()
|
|
59
|
-
mock_response.text = response.text
|
|
60
|
-
mock_response.headers = response.headers
|
|
61
|
-
mock_response.ok = 200 <= response.status_code < 300
|
|
62
|
-
mock_response.content = response.content
|
|
63
|
-
|
|
64
|
-
return mock_response
|
|
65
|
-
|
|
66
|
-
# Remplacer les méthodes du session pour utiliser TestClient
|
|
67
|
-
client.session.request = make_request
|
|
68
|
-
client.session.get = lambda url, **kwargs: make_request("GET", url, **kwargs)
|
|
69
|
-
client.session.post = lambda url, **kwargs: make_request("POST", url, **kwargs)
|
|
70
|
-
client.session.delete = lambda url, **kwargs: make_request("DELETE", url, **kwargs)
|
|
71
|
-
|
|
72
|
-
return client
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class TestProjectIntegration:
|
|
76
|
-
"""Tests d'intégration pour project()."""
|
|
77
|
-
|
|
78
|
-
def test_project_same_result_as_api(self, api_client, sdk_client, simple_spec):
|
|
79
|
-
"""Test que SDK et API donnent les mêmes résultats pour project()."""
|
|
80
|
-
# Test via API directe
|
|
81
|
-
api_response = api_client.post('/project', json=simple_spec)
|
|
82
|
-
assert api_response.status_code == 200
|
|
83
|
-
api_result = api_response.json()
|
|
84
|
-
|
|
85
|
-
# Test via SDK
|
|
86
|
-
sdk_result = sdk_client.project(simple_spec)
|
|
87
|
-
|
|
88
|
-
# Vérifier que les résultats sont identiques
|
|
89
|
-
assert sdk_result['scenarios'] == api_result['scenarios']
|
|
90
|
-
assert sdk_result['steps'] == api_result['steps']
|
|
91
|
-
assert abs(sdk_result['final_values']['capital'] - api_result['final_values']['capital']) < 0.01
|
|
92
|
-
|
|
93
|
-
# Vérifier trajectory
|
|
94
|
-
assert len(sdk_result['trajectory']['capital']) == len(api_result['trajectory']['capital'])
|
|
95
|
-
for i in range(len(sdk_result['trajectory']['capital'])):
|
|
96
|
-
assert abs(sdk_result['trajectory']['capital'][i] - api_result['trajectory']['capital'][i]) < 0.01
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class TestSimulateIntegration:
|
|
100
|
-
"""Tests d'intégration pour simulate()."""
|
|
101
|
-
|
|
102
|
-
def test_simulate_same_result_as_api(self, api_client, sdk_client, simulation_spec):
|
|
103
|
-
"""Test que SDK et API donnent les mêmes résultats pour simulate()."""
|
|
104
|
-
# Test via API directe
|
|
105
|
-
api_response = api_client.post('/simulate', json=simulation_spec)
|
|
106
|
-
assert api_response.status_code == 200
|
|
107
|
-
api_result = api_response.json()
|
|
108
|
-
|
|
109
|
-
# Test via SDK
|
|
110
|
-
sdk_result = sdk_client.simulate(simulation_spec)
|
|
111
|
-
|
|
112
|
-
# Vérifier que les résultats sont identiques
|
|
113
|
-
assert sdk_result['scenarios'] == api_result['scenarios']
|
|
114
|
-
assert sdk_result['steps'] == api_result['steps']
|
|
115
|
-
|
|
116
|
-
# Vérifier last_mean (devrait être identique avec seed explicite)
|
|
117
|
-
# Note: Avec seed explicite, les résultats doivent être identiques
|
|
118
|
-
assert abs(sdk_result['last_mean']['capital'] - api_result['last_mean']['capital']) < 0.01
|
|
119
|
-
|
|
120
|
-
# Vérifier statistics si présentes (devrait être identique avec seed explicite)
|
|
121
|
-
if 'statistics' in api_result and 'capital' in api_result['statistics']:
|
|
122
|
-
api_stats = api_result['statistics']['capital']
|
|
123
|
-
sdk_stats = sdk_result.get('statistics', {}).get('capital', {})
|
|
124
|
-
|
|
125
|
-
# Avec seed explicite, les statistiques doivent être identiques
|
|
126
|
-
if 'mean' in api_stats:
|
|
127
|
-
assert abs(sdk_stats.get('mean', 0) - api_stats['mean']) < 0.01
|
|
128
|
-
if 'median' in api_stats:
|
|
129
|
-
assert abs(sdk_stats.get('median', 0) - api_stats['median']) < 0.01
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class TestBatchIntegration:
|
|
133
|
-
"""Tests d'intégration pour project_batch()."""
|
|
134
|
-
|
|
135
|
-
def test_batch_same_result_as_api(self, api_client, sdk_client, batch_spec_simple):
|
|
136
|
-
"""Test que SDK et API donnent les mêmes résultats pour project_batch()."""
|
|
137
|
-
# Test via API directe
|
|
138
|
-
api_response = api_client.post('/project/batch', json=batch_spec_simple)
|
|
139
|
-
assert api_response.status_code == 200
|
|
140
|
-
api_result = api_response.json()
|
|
141
|
-
|
|
142
|
-
# Test via SDK
|
|
143
|
-
sdk_result = sdk_client.project_batch(
|
|
144
|
-
template=batch_spec_simple['template'],
|
|
145
|
-
batch_params=batch_spec_simple['batch_params']
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# Vérifier que les résultats sont identiques
|
|
149
|
-
assert len(sdk_result['entities']) == len(api_result['entities'])
|
|
150
|
-
|
|
151
|
-
for i, entity in enumerate(sdk_result['entities']):
|
|
152
|
-
api_entity = api_result['entities'][i]
|
|
153
|
-
assert entity['_entity_id'] == api_entity['_entity_id']
|
|
154
|
-
|
|
155
|
-
# Vérifier final_values (liste de dicts, un par scénario)
|
|
156
|
-
if 'final_values' in entity and 'final_values' in api_entity:
|
|
157
|
-
# final_values est une liste de dicts (un par scénario)
|
|
158
|
-
if len(entity['final_values']) > 0 and len(api_entity['final_values']) > 0:
|
|
159
|
-
sdk_fv = entity['final_values'][0] # Premier scénario
|
|
160
|
-
api_fv = api_entity['final_values'][0] # Premier scénario
|
|
161
|
-
for var_name in sdk_fv:
|
|
162
|
-
if var_name in api_fv:
|
|
163
|
-
assert abs(sdk_fv[var_name] - api_fv[var_name]) < 0.01
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class TestValidateIntegration:
|
|
167
|
-
"""Tests d'intégration pour validate()."""
|
|
168
|
-
|
|
169
|
-
def test_validate_same_result_as_api(self, api_client, sdk_client, simple_spec):
|
|
170
|
-
"""Test que SDK et API donnent les mêmes résultats pour validate()."""
|
|
171
|
-
# Test via API directe
|
|
172
|
-
api_response = api_client.post('/validate', json=simple_spec)
|
|
173
|
-
assert api_response.status_code == 200
|
|
174
|
-
api_result = api_response.json()
|
|
175
|
-
|
|
176
|
-
# Test via SDK
|
|
177
|
-
sdk_result = sdk_client.validate(simple_spec)
|
|
178
|
-
|
|
179
|
-
# Vérifier que les résultats sont identiques
|
|
180
|
-
assert sdk_result['valid'] == api_result['valid']
|
|
181
|
-
assert len(sdk_result['errors']) == len(api_result['errors'])
|
|
182
|
-
assert len(sdk_result['warnings']) == len(api_result['warnings'])
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
class TestGetExamplesIntegration:
|
|
186
|
-
"""Tests d'intégration pour get_examples()."""
|
|
187
|
-
|
|
188
|
-
def test_get_examples_same_result_as_api(self, api_client, sdk_client):
|
|
189
|
-
"""Test que SDK et API donnent les mêmes résultats pour get_examples()."""
|
|
190
|
-
# Test via API directe
|
|
191
|
-
api_response = api_client.get('/examples')
|
|
192
|
-
assert api_response.status_code == 200
|
|
193
|
-
api_result = api_response.json()
|
|
194
|
-
|
|
195
|
-
# Test via SDK
|
|
196
|
-
sdk_result = sdk_client.get_examples()
|
|
197
|
-
|
|
198
|
-
# Vérifier que les résultats sont identiques
|
|
199
|
-
assert len(sdk_result['examples']) == len(api_result['examples'])
|
|
200
|
-
|
|
201
|
-
# Vérifier que les noms sont identiques
|
|
202
|
-
sdk_names = [ex.get('name') for ex in sdk_result['examples']]
|
|
203
|
-
api_names = [ex.get('name') for ex in api_result['examples']]
|
|
204
|
-
assert set(sdk_names) == set(api_names)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class TestGetUsageIntegration:
|
|
208
|
-
"""Tests d'intégration pour get_usage()."""
|
|
209
|
-
|
|
210
|
-
def test_get_usage_structure(self, api_client, sdk_client):
|
|
211
|
-
"""Test que get_usage() retourne la bonne structure."""
|
|
212
|
-
# Test via SDK (nécessite API key valide, peut échouer en dev)
|
|
213
|
-
try:
|
|
214
|
-
sdk_result = sdk_client.get_usage()
|
|
215
|
-
|
|
216
|
-
# Vérifier structure de base
|
|
217
|
-
assert 'usage' in sdk_result or 'plan' in sdk_result
|
|
218
|
-
except Exception:
|
|
219
|
-
# En développement sans Supabase, peut échouer
|
|
220
|
-
pytest.skip("get_usage() requires valid API key and Supabase configuration")
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class TestReplayRunIntegration:
|
|
224
|
-
"""Tests d'intégration pour replay_run()."""
|
|
225
|
-
|
|
226
|
-
def test_replay_run_same_result_as_api(self, api_client, sdk_client, monkeypatch):
|
|
227
|
-
"""Test que SDK.replay_run() et API directe donnent les mêmes résultats."""
|
|
228
|
-
import copy
|
|
229
|
-
# FakeSupabase minimal pour le test replay (évite import cross-package)
|
|
230
|
-
class _FakeTableQuery:
|
|
231
|
-
def __init__(self, dataset):
|
|
232
|
-
self.dataset = dataset
|
|
233
|
-
self._filters = []
|
|
234
|
-
self._limit = None
|
|
235
|
-
def select(self, columns="*", count=None): return self
|
|
236
|
-
def match(self, criteria):
|
|
237
|
-
self._filters.append(lambda r, c=criteria: all(r.get(k) == v for k, v in c.items()))
|
|
238
|
-
return self
|
|
239
|
-
def limit(self, v): self._limit = v; return self
|
|
240
|
-
def execute(self):
|
|
241
|
-
rows = [r for r in self.dataset if all(f(r) for f in self._filters)]
|
|
242
|
-
if self._limit: rows = rows[:self._limit]
|
|
243
|
-
return type('R', (), {'data': [copy.deepcopy(r) for r in rows], 'error': None})()
|
|
244
|
-
class _FakeSupabase:
|
|
245
|
-
def __init__(self, tables):
|
|
246
|
-
self.tables = {n: [copy.deepcopy(r) for r in rows] for n, rows in tables.items()}
|
|
247
|
-
|
|
248
|
-
def table(self, name):
|
|
249
|
-
if name not in self.tables:
|
|
250
|
-
self.tables[name] = []
|
|
251
|
-
return _FakeTableQuery(self.tables[name])
|
|
252
|
-
|
|
253
|
-
def rpc(self, name, params):
|
|
254
|
-
"""Mock rpc pour check_monthly_quota, check_replay_quota, etc."""
|
|
255
|
-
class _RpcResult:
|
|
256
|
-
def execute(self):
|
|
257
|
-
return type("R", (), {"data": [{"is_ok": True}], "error": None})()
|
|
258
|
-
return _RpcResult()
|
|
259
|
-
|
|
260
|
-
run_id = "run-replay-integ"
|
|
261
|
-
spec_stored = {
|
|
262
|
-
"meta": {"seed": 999},
|
|
263
|
-
"scenarios": 3,
|
|
264
|
-
"steps": 5,
|
|
265
|
-
"variables": [
|
|
266
|
-
{"name": "taux", "dist": "uniform", "params": {"min": 0.03, "max": 0.07}, "per": "scenario"},
|
|
267
|
-
{"name": "capital", "init": 1000.0, "formula": "capital[t-1] * (1 + taux)"},
|
|
268
|
-
],
|
|
269
|
-
}
|
|
270
|
-
seeds_stored = {"global": 999, "scenarios": [100, 200, 300], "variables": {}}
|
|
271
|
-
projection_runs = [
|
|
272
|
-
{
|
|
273
|
-
"id": run_id,
|
|
274
|
-
"user_id": "user-int",
|
|
275
|
-
"spec": spec_stored,
|
|
276
|
-
"seeds": seeds_stored,
|
|
277
|
-
"scenarios": 3,
|
|
278
|
-
"steps": 5,
|
|
279
|
-
"projection_id": None,
|
|
280
|
-
"api_key_id": "api-key-int",
|
|
281
|
-
}
|
|
282
|
-
]
|
|
283
|
-
tables = {"projection_runs": projection_runs, "projections": [], "run_artifacts": []}
|
|
284
|
-
fake_supabase = _FakeSupabase(tables)
|
|
285
|
-
|
|
286
|
-
async def fake_require(api_key):
|
|
287
|
-
return "business", {
|
|
288
|
-
"user_id": "user-int",
|
|
289
|
-
"api_key_id": "api-key-int",
|
|
290
|
-
"max_cpus": 4,
|
|
291
|
-
"max_scenarios": 1000,
|
|
292
|
-
"name": "Business",
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
monkeypatch.setattr("api.main.require_authenticated_user", fake_require)
|
|
296
|
-
monkeypatch.setattr("api.main.get_supabase_or_503", lambda: fake_supabase)
|
|
297
|
-
|
|
298
|
-
# API directe
|
|
299
|
-
api_response = api_client.post(
|
|
300
|
-
f"/projection-runs/{run_id}/replay/1",
|
|
301
|
-
headers={"X-API-Key": "test-key"}
|
|
302
|
-
)
|
|
303
|
-
if api_response.status_code != 200:
|
|
304
|
-
pytest.skip(
|
|
305
|
-
f"Replay requires full Supabase mock (INSERT, etc.): "
|
|
306
|
-
f"{api_response.status_code} - {api_response.json().get('detail', '')}"
|
|
307
|
-
)
|
|
308
|
-
api_result = api_response.json()
|
|
309
|
-
|
|
310
|
-
# SDK
|
|
311
|
-
sdk_result = sdk_client.replay_run(run_id=run_id, scenario_id=1)
|
|
312
|
-
|
|
313
|
-
# Vérifier résultats identiques
|
|
314
|
-
assert sdk_result["scenarios"] == api_result["scenarios"]
|
|
315
|
-
assert sdk_result["steps"] == api_result["steps"]
|
|
316
|
-
assert sdk_result["run_id"] == api_result["run_id"]
|
|
317
|
-
sdk_fv = sdk_result["final_values"][0] if isinstance(sdk_result["final_values"], list) else sdk_result["final_values"]
|
|
318
|
-
api_fv = api_result["final_values"][0] if isinstance(api_result["final_values"], list) else api_result["final_values"]
|
|
319
|
-
assert abs(sdk_fv.get("capital", 0) - api_fv.get("capital", 0)) < 0.01
|
|
320
|
-
|
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests unitaires pour le SDK Python (avec mocks).
|
|
3
|
-
|
|
4
|
-
Ces tests vérifient le comportement du SDK sans dépendre de l'API réelle.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
from unittest.mock import Mock, patch
|
|
9
|
-
from subspacecomputing import (
|
|
10
|
-
ASTRIA,
|
|
11
|
-
SubspaceError,
|
|
12
|
-
QuotaExceededError,
|
|
13
|
-
RateLimitError,
|
|
14
|
-
AuthenticationError,
|
|
15
|
-
ValidationError,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestClientInitialization:
|
|
20
|
-
"""Tests d'initialisation du client."""
|
|
21
|
-
|
|
22
|
-
def test_init_with_api_key(self, api_key, base_url):
|
|
23
|
-
"""Test initialisation avec API key."""
|
|
24
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
25
|
-
assert client.api_key == api_key
|
|
26
|
-
assert client.base_url == base_url
|
|
27
|
-
assert 'X-API-Key' in client.session.headers
|
|
28
|
-
assert client.session.headers['X-API-Key'] == api_key
|
|
29
|
-
|
|
30
|
-
def test_init_default_base_url(self, api_key):
|
|
31
|
-
"""Test initialisation avec URL par défaut."""
|
|
32
|
-
client = ASTRIA(api_key=api_key)
|
|
33
|
-
assert client.base_url == 'https://api.subspacecomputing.com'
|
|
34
|
-
|
|
35
|
-
def test_init_strips_trailing_slash(self, api_key):
|
|
36
|
-
"""Test que l'URL est nettoyée (pas de slash final)."""
|
|
37
|
-
client = ASTRIA(api_key=api_key, base_url='http://localhost:8000/')
|
|
38
|
-
assert client.base_url == 'http://localhost:8000'
|
|
39
|
-
|
|
40
|
-
def test_init_rejects_empty_api_key(self):
|
|
41
|
-
"""Test que api_key vide lève ValueError."""
|
|
42
|
-
with pytest.raises(ValueError, match="API key is required"):
|
|
43
|
-
ASTRIA(api_key="")
|
|
44
|
-
with pytest.raises(ValueError, match="API key is required"):
|
|
45
|
-
ASTRIA(api_key=" ")
|
|
46
|
-
with pytest.raises(ValueError, match="API key is required"):
|
|
47
|
-
ASTRIA(api_key=None)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class TestErrorHandling:
|
|
51
|
-
"""Tests de gestion des erreurs HTTP."""
|
|
52
|
-
|
|
53
|
-
def test_rate_limit_error(self, api_key, base_url):
|
|
54
|
-
"""Test gestion erreur 429 (rate limit)."""
|
|
55
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
56
|
-
|
|
57
|
-
mock_response = Mock()
|
|
58
|
-
mock_response.status_code = 429
|
|
59
|
-
mock_response.text = "Rate limit exceeded"
|
|
60
|
-
mock_response.headers = {"Retry-After": "60"}
|
|
61
|
-
mock_response.ok = False
|
|
62
|
-
|
|
63
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
64
|
-
with pytest.raises(RateLimitError) as exc_info:
|
|
65
|
-
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
66
|
-
assert "Rate limit" in str(exc_info.value)
|
|
67
|
-
|
|
68
|
-
def test_quota_exceeded_error(self, api_key, base_url):
|
|
69
|
-
"""Test gestion erreur 429 (quota mensuel)."""
|
|
70
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
71
|
-
|
|
72
|
-
mock_response = Mock()
|
|
73
|
-
mock_response.status_code = 429
|
|
74
|
-
mock_response.text = "Monthly quota exceeded"
|
|
75
|
-
mock_response.headers = {}
|
|
76
|
-
mock_response.ok = False
|
|
77
|
-
|
|
78
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
79
|
-
with pytest.raises(QuotaExceededError) as exc_info:
|
|
80
|
-
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
81
|
-
assert "quota" in str(exc_info.value).lower()
|
|
82
|
-
|
|
83
|
-
def test_authentication_error(self, api_key, base_url):
|
|
84
|
-
"""Test gestion erreur 401 (API key invalide)."""
|
|
85
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
86
|
-
|
|
87
|
-
mock_response = Mock()
|
|
88
|
-
mock_response.status_code = 401
|
|
89
|
-
mock_response.text = "Invalid API key"
|
|
90
|
-
mock_response.ok = False
|
|
91
|
-
|
|
92
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
93
|
-
with pytest.raises(AuthenticationError) as exc_info:
|
|
94
|
-
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
95
|
-
assert "API key" in str(exc_info.value)
|
|
96
|
-
|
|
97
|
-
def test_validation_error(self, api_key, base_url):
|
|
98
|
-
"""Test gestion erreur 400 (validation)."""
|
|
99
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
100
|
-
|
|
101
|
-
mock_response = Mock()
|
|
102
|
-
mock_response.status_code = 400
|
|
103
|
-
mock_response.text = "Invalid SP Model"
|
|
104
|
-
mock_response.json.return_value = {'detail': 'Invalid formula'}
|
|
105
|
-
mock_response.ok = False
|
|
106
|
-
|
|
107
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
108
|
-
with pytest.raises(ValidationError) as exc_info:
|
|
109
|
-
client.validate({'scenarios': 1, 'steps': 12, 'variables': []})
|
|
110
|
-
assert "validation" in str(exc_info.value).lower()
|
|
111
|
-
|
|
112
|
-
def test_generic_error(self, api_key, base_url):
|
|
113
|
-
"""Test gestion erreur générique (500)."""
|
|
114
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
115
|
-
|
|
116
|
-
mock_response = Mock()
|
|
117
|
-
mock_response.status_code = 500
|
|
118
|
-
mock_response.text = "Internal server error"
|
|
119
|
-
mock_response.ok = False
|
|
120
|
-
|
|
121
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
122
|
-
with pytest.raises(SubspaceError) as exc_info:
|
|
123
|
-
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
124
|
-
assert "500" in str(exc_info.value)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class TestEndpoints:
|
|
128
|
-
"""Tests des endpoints du SDK."""
|
|
129
|
-
|
|
130
|
-
def test_project_success(self, api_key, base_url, simple_spec):
|
|
131
|
-
"""Test endpoint project() avec succès."""
|
|
132
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
133
|
-
|
|
134
|
-
mock_response = Mock()
|
|
135
|
-
mock_response.status_code = 200
|
|
136
|
-
mock_response.ok = True
|
|
137
|
-
mock_response.json.return_value = {
|
|
138
|
-
'scenarios': 1,
|
|
139
|
-
'steps': 12,
|
|
140
|
-
'final_values': {'capital': 1795.86},
|
|
141
|
-
'trajectory': {'capital': [1000.0, 1050.0, 1102.5]},
|
|
142
|
-
'performance': {'execution_time_ms': 0.5}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
146
|
-
result = client.project(simple_spec)
|
|
147
|
-
assert result['scenarios'] == 1
|
|
148
|
-
assert 'final_values' in result
|
|
149
|
-
assert 'trajectory' in result
|
|
150
|
-
|
|
151
|
-
def test_simulate_success(self, api_key, base_url, simulation_spec):
|
|
152
|
-
"""Test endpoint simulate() avec succès."""
|
|
153
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
154
|
-
|
|
155
|
-
mock_response = Mock()
|
|
156
|
-
mock_response.status_code = 200
|
|
157
|
-
mock_response.ok = True
|
|
158
|
-
mock_response.json.return_value = {
|
|
159
|
-
'scenarios': 100,
|
|
160
|
-
'steps': 12,
|
|
161
|
-
'last_mean': {'capital': 1712.34},
|
|
162
|
-
'statistics': {
|
|
163
|
-
'capital': {
|
|
164
|
-
'mean': 1712.34,
|
|
165
|
-
'median': 1703.55,
|
|
166
|
-
'percentiles': {'5': 1650.0, '95': 1780.0}
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
'performance': {'execution_time_ms': 12.5}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
173
|
-
result = client.simulate(simulation_spec)
|
|
174
|
-
assert result['scenarios'] == 100
|
|
175
|
-
assert 'last_mean' in result
|
|
176
|
-
assert 'statistics' in result
|
|
177
|
-
|
|
178
|
-
def test_project_batch_success(self, api_key, base_url, batch_spec_simple):
|
|
179
|
-
"""Test endpoint project_batch() avec succès."""
|
|
180
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
181
|
-
|
|
182
|
-
mock_response = Mock()
|
|
183
|
-
mock_response.status_code = 200
|
|
184
|
-
mock_response.ok = True
|
|
185
|
-
mock_response.json.return_value = {
|
|
186
|
-
'entities': [
|
|
187
|
-
{
|
|
188
|
-
'_entity_id': 'emp_001',
|
|
189
|
-
'final_values': {'capital': 1795.86}
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
'_entity_id': 'emp_002',
|
|
193
|
-
'final_values': {'capital': 3591.72}
|
|
194
|
-
}
|
|
195
|
-
],
|
|
196
|
-
'aggregations': {},
|
|
197
|
-
'performance': {'execution_time_ms': 25.0}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
201
|
-
result = client.project_batch(
|
|
202
|
-
template=batch_spec_simple['template'],
|
|
203
|
-
batch_params=batch_spec_simple['batch_params']
|
|
204
|
-
)
|
|
205
|
-
assert len(result['entities']) == 2
|
|
206
|
-
assert result['entities'][0]['_entity_id'] == 'emp_001'
|
|
207
|
-
|
|
208
|
-
def test_validate_success(self, api_key, base_url, simple_spec):
|
|
209
|
-
"""Test endpoint validate() avec succès."""
|
|
210
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
211
|
-
|
|
212
|
-
mock_response = Mock()
|
|
213
|
-
mock_response.status_code = 200
|
|
214
|
-
mock_response.ok = True
|
|
215
|
-
mock_response.json.return_value = {
|
|
216
|
-
'valid': True,
|
|
217
|
-
'errors': [],
|
|
218
|
-
'warnings': []
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
222
|
-
result = client.validate(simple_spec)
|
|
223
|
-
assert result['valid'] is True
|
|
224
|
-
assert len(result['errors']) == 0
|
|
225
|
-
|
|
226
|
-
def test_get_examples_success(self, api_key, base_url):
|
|
227
|
-
"""Test endpoint get_examples() avec succès."""
|
|
228
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
229
|
-
|
|
230
|
-
mock_response = Mock()
|
|
231
|
-
mock_response.status_code = 200
|
|
232
|
-
mock_response.ok = True
|
|
233
|
-
mock_response.json.return_value = {
|
|
234
|
-
'examples': [
|
|
235
|
-
{'name': 'Intérêt Simple', 'spec': {}},
|
|
236
|
-
{'name': 'Portfolio Simple', 'spec': {}}
|
|
237
|
-
]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
client.session.get = Mock(return_value=mock_response)
|
|
241
|
-
result = client.get_examples()
|
|
242
|
-
assert len(result['examples']) == 2
|
|
243
|
-
|
|
244
|
-
def test_get_usage_success(self, api_key, base_url):
|
|
245
|
-
"""Test endpoint get_usage() avec succès."""
|
|
246
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
247
|
-
|
|
248
|
-
mock_response = Mock()
|
|
249
|
-
mock_response.status_code = 200
|
|
250
|
-
mock_response.ok = True
|
|
251
|
-
mock_response.json.return_value = {
|
|
252
|
-
'usage': {
|
|
253
|
-
'simulations_used': 45,
|
|
254
|
-
'simulations_limit': 50
|
|
255
|
-
},
|
|
256
|
-
'plan': {
|
|
257
|
-
'name': 'Essentials',
|
|
258
|
-
'max_scenarios_per_request': 1000
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
with patch.object(client.session, 'get', return_value=mock_response):
|
|
263
|
-
result = client.get_usage()
|
|
264
|
-
assert 'usage' in result
|
|
265
|
-
assert result['usage']['simulations_used'] == 45
|
|
266
|
-
|
|
267
|
-
def test_replay_run_success(self, api_key, base_url):
|
|
268
|
-
"""Test endpoint replay_run() avec succès."""
|
|
269
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
270
|
-
|
|
271
|
-
mock_response = Mock()
|
|
272
|
-
mock_response.status_code = 200
|
|
273
|
-
mock_response.ok = True
|
|
274
|
-
mock_response.json.return_value = {
|
|
275
|
-
'scenarios': 1,
|
|
276
|
-
'steps': 6,
|
|
277
|
-
'final_values': [{'capital': 1298.39, 'taux': 0.053}],
|
|
278
|
-
'last_mean': {'capital': 1298.39, 'taux': 0.053},
|
|
279
|
-
'statistics': {'capital': {'mean': 1298.39}},
|
|
280
|
-
'performance': {'execution_time_ms': 15.2},
|
|
281
|
-
'run_id': 'run-123'
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
with patch.object(client.session, 'post', return_value=mock_response):
|
|
285
|
-
result = client.replay_run(run_id='run-123', scenario_id=2)
|
|
286
|
-
assert result['scenarios'] == 1
|
|
287
|
-
assert result['steps'] == 6
|
|
288
|
-
assert 'final_values' in result
|
|
289
|
-
assert result['run_id'] == 'run-123'
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
class TestBatchParameters:
|
|
293
|
-
"""Tests de gestion des paramètres batch."""
|
|
294
|
-
|
|
295
|
-
def test_project_batch_with_aggregations(self, api_key, base_url):
|
|
296
|
-
"""Test project_batch() avec agrégations."""
|
|
297
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
298
|
-
|
|
299
|
-
template = {
|
|
300
|
-
'scenarios': 1,
|
|
301
|
-
'steps': 12,
|
|
302
|
-
'variables': [
|
|
303
|
-
{'name': 'capital', 'init': 'batch_params.initial', 'formula': 'capital[t-1] * 1.05'}
|
|
304
|
-
]
|
|
305
|
-
}
|
|
306
|
-
batch_params = [
|
|
307
|
-
{'entity_id': 'emp_001', 'initial': 1000},
|
|
308
|
-
{'entity_id': 'emp_002', 'initial': 2000}
|
|
309
|
-
]
|
|
310
|
-
aggregations = [
|
|
311
|
-
{'name': 'capital_total', 'formula': 'sum(capital[t_final])'}
|
|
312
|
-
]
|
|
313
|
-
|
|
314
|
-
mock_response = Mock()
|
|
315
|
-
mock_response.status_code = 200
|
|
316
|
-
mock_response.ok = True
|
|
317
|
-
mock_response.json.return_value = {
|
|
318
|
-
'entities': [],
|
|
319
|
-
'aggregations': {'capital_total': 3000.0}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
with patch.object(client.session, 'post', return_value=mock_response) as mock_post:
|
|
323
|
-
result = client.project_batch(
|
|
324
|
-
template=template,
|
|
325
|
-
batch_params=batch_params,
|
|
326
|
-
aggregations=aggregations
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
# Vérifier que les agrégations sont bien envoyées
|
|
330
|
-
call_args = mock_post.call_args
|
|
331
|
-
assert 'aggregations' in call_args[1]['json']
|
|
332
|
-
assert result['aggregations']['capital_total'] == 3000.0
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
class TestRateLimitQuotaInfo:
|
|
336
|
-
"""Tests for get_rate_limit_info and get_quota_info with _safe_int."""
|
|
337
|
-
|
|
338
|
-
def test_get_rate_limit_info_unlimited(self, api_key, base_url):
|
|
339
|
-
"""get_rate_limit_info handles 'unlimited' without crashing."""
|
|
340
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
341
|
-
mock_response = Mock()
|
|
342
|
-
mock_response.headers = {"X-RateLimit-Limit": "unlimited", "X-RateLimit-Remaining": "999"}
|
|
343
|
-
client.last_response = mock_response
|
|
344
|
-
|
|
345
|
-
result = client.get_rate_limit_info()
|
|
346
|
-
assert result["limit"] is None
|
|
347
|
-
assert result["remaining"] == 999
|
|
348
|
-
|
|
349
|
-
def test_get_quota_info_unlimited(self, api_key, base_url):
|
|
350
|
-
"""get_quota_info handles 'unlimited' in X-Quota-Limit."""
|
|
351
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
352
|
-
mock_response = Mock()
|
|
353
|
-
mock_response.headers = {
|
|
354
|
-
"X-Quota-Limit": "unlimited",
|
|
355
|
-
"X-Quota-Remaining": "100",
|
|
356
|
-
"X-Quota-Used": "50",
|
|
357
|
-
}
|
|
358
|
-
client.last_response = mock_response
|
|
359
|
-
|
|
360
|
-
result = client.get_quota_info()
|
|
361
|
-
assert result["limit"] is None
|
|
362
|
-
assert result["remaining"] == 100
|
|
363
|
-
assert result["used"] == 50
|
|
364
|
-
|
|
365
|
-
def test_get_quota_info_invalid_values(self, api_key, base_url):
|
|
366
|
-
"""get_quota_info handles non-numeric values gracefully."""
|
|
367
|
-
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
368
|
-
mock_response = Mock()
|
|
369
|
-
mock_response.headers = {
|
|
370
|
-
"X-Quota-Limit": "N/A",
|
|
371
|
-
"X-Quota-Remaining": "remaining",
|
|
372
|
-
"X-Quota-Used": "50",
|
|
373
|
-
}
|
|
374
|
-
client.last_response = mock_response
|
|
375
|
-
|
|
376
|
-
result = client.get_quota_info()
|
|
377
|
-
assert result["limit"] is None
|
|
378
|
-
assert result["remaining"] is None
|
|
379
|
-
assert result["used"] == 50
|
|
380
|
-
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for pandas integration utilities.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
pandas = pytest.importorskip("pandas")
|
|
8
|
-
|
|
9
|
-
from subspacecomputing.utils.pandas_integration import batch_response_to_dataframe
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestBatchResponseToDataframe:
|
|
13
|
-
"""Tests for batch_response_to_dataframe."""
|
|
14
|
-
|
|
15
|
-
def test_entities_null_does_not_crash(self):
|
|
16
|
-
"""batch_response_to_dataframe handles entities=null without TypeError."""
|
|
17
|
-
response = {"entities": None, "aggregations": {}, "summary": {}}
|
|
18
|
-
df = batch_response_to_dataframe(response)
|
|
19
|
-
assert len(df) == 0
|
|
20
|
-
|
|
21
|
-
def test_entities_missing_uses_empty_list(self):
|
|
22
|
-
"""batch_response_to_dataframe handles missing entities key."""
|
|
23
|
-
response = {"aggregations": {}, "summary": {}}
|
|
24
|
-
df = batch_response_to_dataframe(response)
|
|
25
|
-
assert len(df) == 0
|
|
26
|
-
|
|
27
|
-
def test_entities_normal(self):
|
|
28
|
-
"""batch_response_to_dataframe converts entities to DataFrame."""
|
|
29
|
-
response = {
|
|
30
|
-
"entities": [
|
|
31
|
-
{"_entity_id": "e1", "final_values": {"capital": 1000, "taux": 0.05}},
|
|
32
|
-
{"_entity_id": "e2", "final_values": {"capital": 2000, "taux": 0.06}},
|
|
33
|
-
],
|
|
34
|
-
"aggregations": {},
|
|
35
|
-
"summary": {},
|
|
36
|
-
}
|
|
37
|
-
df = batch_response_to_dataframe(response)
|
|
38
|
-
assert len(df) == 2
|
|
39
|
-
assert list(df.columns) == ["entity_id", "capital", "taux"]
|
|
40
|
-
assert df["entity_id"].tolist() == ["e1", "e2"]
|
|
41
|
-
assert df["capital"].tolist() == [1000, 2000]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/utils/pandas_integration.py
RENAMED
|
File without changes
|
{subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/top_level.txt
RENAMED
|
File without changes
|