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.
Files changed (20) hide show
  1. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/LICENSE +1 -1
  2. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/MANIFEST.in +6 -0
  3. {subspacecomputing-0.1.4/subspacecomputing.egg-info → subspacecomputing-0.1.5}/PKG-INFO +48 -7
  4. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/README.md +45 -4
  5. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/pyproject.toml +2 -2
  6. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/setup.py +4 -4
  7. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/__init__.py +1 -1
  8. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/client.py +98 -0
  9. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5/subspacecomputing.egg-info}/PKG-INFO +48 -7
  10. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/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.5}/setup.cfg +0 -0
  15. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/errors.py +0 -0
  16. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/utils/__init__.py +0 -0
  17. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing/utils/pandas_integration.py +0 -0
  18. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/dependency_links.txt +0 -0
  19. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/subspacecomputing.egg-info/requires.txt +0 -0
  20. {subspacecomputing-0.1.4 → subspacecomputing-0.1.5}/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.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@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
 
@@ -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@beausoft.ca
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
- © 2025 Beausoft Inc. All Rights Reserved
311
+ © 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
 
@@ -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@beausoft.ca
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
- © 2025 Beausoft Inc. All Rights Reserved
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.4"
9
+ version = "0.1.5"
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.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@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.5"
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,
@@ -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.4
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@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
 
@@ -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@beausoft.ca
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
- © 2025 Beausoft Inc. All Rights Reserved
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]