subspacecomputing 0.1.0__tar.gz → 0.1.2__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.0/subspacecomputing.egg-info → subspacecomputing-0.1.2}/PKG-INFO +13 -2
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/README.md +12 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/pyproject.toml +2 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/setup.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/__init__.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/client.py +66 -19
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/errors.py +16 -4
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/utils/pandas_integration.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2/subspacecomputing.egg-info}/PKG-INFO +13 -2
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/SOURCES.txt +2 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/tests/test_client_unit.py +47 -0
- subspacecomputing-0.1.2/tests/test_pandas_integration.py +41 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/LICENSE +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/MANIFEST.in +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/setup.cfg +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/utils/__init__.py +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/dependency_links.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/requires.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/top_level.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/tests/test_client_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subspacecomputing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for Subspace Computing Engine API (by Beausoft)
|
|
5
5
|
Home-page: https://www.subspacecomputing.com/developer
|
|
6
6
|
Author: Beausoft
|
|
@@ -49,6 +49,17 @@ bsce = BSCE(api_key='your-api-key-here')
|
|
|
49
49
|
# bsce = BSCE(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Teams (`X-Team-Id`)
|
|
53
|
+
|
|
54
|
+
Keys can carry a `default_team_id` from the portal. If the user is in **several** teams, some endpoints require an explicit team: pass `team_id=...` or call `set_team_id` so the SDK sends `X-Team-Id`.
|
|
55
|
+
|
|
56
|
+
If you omit it when required, the API returns **400** with `error.reason` **`team_context_required`**. Use `ValidationError` and read `exc.reason`.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
bsce = BSCE(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
60
|
+
bsce.set_team_id(None) # clear override for following requests
|
|
61
|
+
```
|
|
62
|
+
|
|
52
63
|
### Simple Projection (1 scenario)
|
|
53
64
|
|
|
54
65
|
```python
|
|
@@ -245,7 +256,7 @@ if quota:
|
|
|
245
256
|
|
|
246
257
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
247
258
|
|
|
248
|
-
API reference is available at https://
|
|
259
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
249
260
|
|
|
250
261
|
For support, reach out to contact@beausoft.ca
|
|
251
262
|
|
|
@@ -22,6 +22,17 @@ bsce = BSCE(api_key='your-api-key-here')
|
|
|
22
22
|
# bsce = BSCE(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
### Teams (`X-Team-Id`)
|
|
26
|
+
|
|
27
|
+
Keys can carry a `default_team_id` from the portal. If the user is in **several** teams, some endpoints require an explicit team: pass `team_id=...` or call `set_team_id` so the SDK sends `X-Team-Id`.
|
|
28
|
+
|
|
29
|
+
If you omit it when required, the API returns **400** with `error.reason` **`team_context_required`**. Use `ValidationError` and read `exc.reason`.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
bsce = BSCE(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
33
|
+
bsce.set_team_id(None) # clear override for following requests
|
|
34
|
+
```
|
|
35
|
+
|
|
25
36
|
### Simple Projection (1 scenario)
|
|
26
37
|
|
|
27
38
|
```python
|
|
@@ -218,7 +229,7 @@ if quota:
|
|
|
218
229
|
|
|
219
230
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
220
231
|
|
|
221
|
-
API reference is available at https://
|
|
232
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
222
233
|
|
|
223
234
|
For support, reach out to contact@beausoft.ca
|
|
224
235
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "subspacecomputing"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.2"
|
|
10
10
|
description = "Python SDK for Subspace Computing Engine API (by Beausoft)"
|
|
11
11
|
authors = [{name = "Beausoft", email = "contact@beausoft.ca"}]
|
|
12
12
|
readme = "README.md"
|
|
@@ -25,6 +25,7 @@ dev = [
|
|
|
25
25
|
"fastapi>=0.100.0", # Pour tests d'intégration
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
+
# No Git repository URL: built from a private monorepo; only public product URLs are published on PyPI.
|
|
28
29
|
[project.urls]
|
|
29
30
|
Documentation = "https://www.subspacecomputing.com/developer"
|
|
30
31
|
Homepage = "https://www.subspacecomputing.com/developer"
|
|
@@ -13,7 +13,7 @@ except FileNotFoundError:
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="subspacecomputing",
|
|
16
|
-
version="0.1.
|
|
16
|
+
version="0.1.2",
|
|
17
17
|
description="Python SDK for Subspace Computing Engine API (by Beausoft)",
|
|
18
18
|
long_description=long_description,
|
|
19
19
|
long_description_content_type="text/markdown",
|
|
@@ -26,6 +26,8 @@ class BSCE:
|
|
|
26
26
|
api_key: str,
|
|
27
27
|
base_url: Optional[str] = None,
|
|
28
28
|
timeout: Optional[int] = None,
|
|
29
|
+
*,
|
|
30
|
+
team_id: Optional[str] = None,
|
|
29
31
|
):
|
|
30
32
|
"""
|
|
31
33
|
Initialize the BSCE client.
|
|
@@ -36,6 +38,7 @@ class BSCE:
|
|
|
36
38
|
base_url: API base URL (optional, defaults to https://api.subspacecomputing.com).
|
|
37
39
|
Only useful for local testing or custom environments.
|
|
38
40
|
timeout: Request timeout in seconds (optional, defaults to 60).
|
|
41
|
+
team_id: Optional team UUID for header ``X-Team-Id`` (override default_team_id on the key).
|
|
39
42
|
|
|
40
43
|
Raises:
|
|
41
44
|
ValueError: If api_key is empty or None.
|
|
@@ -47,13 +50,25 @@ class BSCE:
|
|
|
47
50
|
self.api_key = api_key.strip()
|
|
48
51
|
self.base_url = (base_url or "https://api.subspacecomputing.com").rstrip("/")
|
|
49
52
|
self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
53
|
+
self._team_id = (team_id or "").strip() or None
|
|
50
54
|
self.session = requests.Session()
|
|
51
|
-
|
|
55
|
+
headers = {
|
|
52
56
|
"X-API-Key": self.api_key,
|
|
53
57
|
"Content-Type": "application/json",
|
|
54
|
-
}
|
|
58
|
+
}
|
|
59
|
+
if self._team_id:
|
|
60
|
+
headers["X-Team-Id"] = self._team_id
|
|
61
|
+
self.session.headers.update(headers)
|
|
55
62
|
self.last_response = None # Store last response for header access
|
|
56
63
|
|
|
64
|
+
def set_team_id(self, team_id: Optional[str] = None) -> None:
|
|
65
|
+
"""Set or clear ``X-Team-Id`` for subsequent requests."""
|
|
66
|
+
self._team_id = (team_id or "").strip() or None
|
|
67
|
+
if self._team_id:
|
|
68
|
+
self.session.headers["X-Team-Id"] = self._team_id
|
|
69
|
+
else:
|
|
70
|
+
self.session.headers.pop("X-Team-Id", None)
|
|
71
|
+
|
|
57
72
|
def _handle_response(self, response: requests.Response):
|
|
58
73
|
"""
|
|
59
74
|
Handle HTTP errors and raise appropriate exceptions.
|
|
@@ -140,15 +155,15 @@ class BSCE:
|
|
|
140
155
|
scenario_id: int,
|
|
141
156
|
) -> Dict[str, Any]:
|
|
142
157
|
"""
|
|
143
|
-
|
|
158
|
+
Replay a specific scenario from a previous simulation.
|
|
144
159
|
|
|
145
160
|
Args:
|
|
146
|
-
original_spec: SP Model
|
|
147
|
-
original_seeds:
|
|
148
|
-
scenario_id: Index
|
|
161
|
+
original_spec: Full SP Model from the initial simulation
|
|
162
|
+
original_seeds: Seeds object from simulate() response (seeds.global, seeds.scenarios)
|
|
163
|
+
scenario_id: Index of the scenario to replay (0 to scenarios-1)
|
|
149
164
|
|
|
150
165
|
Returns:
|
|
151
|
-
|
|
166
|
+
Simulate response for this single scenario (final_values, sample_path_s0, etc.)
|
|
152
167
|
|
|
153
168
|
Example:
|
|
154
169
|
result = client.simulate(spec)
|
|
@@ -401,23 +416,24 @@ class BSCE:
|
|
|
401
416
|
|
|
402
417
|
def replay_run(self, run_id: str, scenario_id: int) -> Dict[str, Any]:
|
|
403
418
|
"""
|
|
404
|
-
|
|
419
|
+
Replay a scenario from a stored run (requires persisted seeds).
|
|
405
420
|
|
|
406
421
|
Args:
|
|
407
|
-
run_id: ID
|
|
408
|
-
scenario_id: Index
|
|
422
|
+
run_id: Run ID (created with meta.store_seeds_for_replay: true)
|
|
423
|
+
scenario_id: Index of the scenario to replay (0 to scenarios-1)
|
|
409
424
|
|
|
410
425
|
Returns:
|
|
411
|
-
|
|
426
|
+
Simulate response for this single scenario (final_values, sample_path_s0, etc.)
|
|
412
427
|
|
|
413
428
|
Raises:
|
|
414
|
-
ValidationError:
|
|
415
|
-
AuthenticationError:
|
|
429
|
+
ValidationError: If seeds not stored or scenario_id out of range
|
|
430
|
+
AuthenticationError: If API key invalid
|
|
416
431
|
|
|
417
432
|
Example:
|
|
418
|
-
result = client.simulate(spec) #
|
|
433
|
+
result = client.simulate(spec) # with store_seeds_for_replay: true
|
|
419
434
|
replay = client.replay_run(result["run_id"], scenario_id=12)
|
|
420
435
|
"""
|
|
436
|
+
# API expects run_id and scenario_id in URL path; no request body required
|
|
421
437
|
response = self.session.post(
|
|
422
438
|
f"{self.base_url}/projection-runs/{run_id}/replay/{scenario_id}",
|
|
423
439
|
timeout=self.timeout,
|
|
@@ -444,8 +460,39 @@ class BSCE:
|
|
|
444
460
|
self._handle_response(response)
|
|
445
461
|
# 204 No Content, no body
|
|
446
462
|
|
|
463
|
+
def get_run_artifact(self, artifact_id: str) -> Dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Fetch a run artifact by id (authZ: view on parent projection).
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
artifact_id: run_artifacts.id
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Artifact payload (may include final_values or url).
|
|
472
|
+
"""
|
|
473
|
+
response = self.session.get(
|
|
474
|
+
f"{self.base_url}/run-artifacts/{artifact_id}",
|
|
475
|
+
timeout=self.timeout,
|
|
476
|
+
)
|
|
477
|
+
self.last_response = response
|
|
478
|
+
self._handle_response(response)
|
|
479
|
+
return response.json()
|
|
480
|
+
|
|
447
481
|
# Helper methods for accessing rate limit and quota info
|
|
448
482
|
|
|
483
|
+
@staticmethod
|
|
484
|
+
def _safe_int(val: Optional[str]) -> Optional[int]:
|
|
485
|
+
"""
|
|
486
|
+
Safely convert a header value to int.
|
|
487
|
+
Returns None for None, 'unlimited', or non-numeric strings.
|
|
488
|
+
"""
|
|
489
|
+
if val is None or val == "unlimited":
|
|
490
|
+
return None
|
|
491
|
+
try:
|
|
492
|
+
return int(val)
|
|
493
|
+
except (ValueError, TypeError):
|
|
494
|
+
return None
|
|
495
|
+
|
|
449
496
|
def get_rate_limit_info(self) -> Optional[Dict[str, Any]]:
|
|
450
497
|
"""
|
|
451
498
|
Get rate limit information from the last response.
|
|
@@ -460,8 +507,8 @@ class BSCE:
|
|
|
460
507
|
if limit is None and remaining is None:
|
|
461
508
|
return None
|
|
462
509
|
return {
|
|
463
|
-
"limit":
|
|
464
|
-
"remaining":
|
|
510
|
+
"limit": self._safe_int(limit),
|
|
511
|
+
"remaining": self._safe_int(remaining),
|
|
465
512
|
}
|
|
466
513
|
|
|
467
514
|
def get_quota_info(self) -> Optional[Dict[str, Any]]:
|
|
@@ -479,7 +526,7 @@ class BSCE:
|
|
|
479
526
|
if limit is None and remaining is None and used is None:
|
|
480
527
|
return None
|
|
481
528
|
return {
|
|
482
|
-
"limit":
|
|
483
|
-
"remaining":
|
|
484
|
-
"used":
|
|
529
|
+
"limit": self._safe_int(limit),
|
|
530
|
+
"remaining": self._safe_int(remaining),
|
|
531
|
+
"used": self._safe_int(used),
|
|
485
532
|
}
|
|
@@ -3,7 +3,7 @@ Custom exceptions for the Subspace Computing SDK.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Optional
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class BSCEError(Exception):
|
|
@@ -13,19 +13,31 @@ class BSCEError(Exception):
|
|
|
13
13
|
self.message = message
|
|
14
14
|
self.response = response
|
|
15
15
|
self.status_code = response.status_code if response else None
|
|
16
|
-
self.detail = None
|
|
16
|
+
self.detail: Any = None
|
|
17
|
+
self._body: Optional[dict] = None
|
|
17
18
|
|
|
18
|
-
# Try to extract error details from the response
|
|
19
19
|
if response is not None:
|
|
20
20
|
try:
|
|
21
21
|
error_data = response.json()
|
|
22
22
|
if isinstance(error_data, dict):
|
|
23
|
+
self._body = error_data
|
|
23
24
|
self.detail = error_data.get("detail", error_data.get("message"))
|
|
24
|
-
except
|
|
25
|
+
except ValueError:
|
|
25
26
|
self.detail = response.text
|
|
26
27
|
|
|
27
28
|
super().__init__(self.message)
|
|
28
29
|
|
|
30
|
+
@property
|
|
31
|
+
def reason(self) -> Optional[str]:
|
|
32
|
+
"""Clé stable contrat §6.8 si le corps est ``{\"error\": {\"reason\": ...}}``."""
|
|
33
|
+
if not self._body:
|
|
34
|
+
return None
|
|
35
|
+
err = self._body.get("error")
|
|
36
|
+
if isinstance(err, dict):
|
|
37
|
+
r = err.get("reason")
|
|
38
|
+
return str(r) if r is not None else None
|
|
39
|
+
return None
|
|
40
|
+
|
|
29
41
|
def __str__(self):
|
|
30
42
|
if self.detail:
|
|
31
43
|
return f"{self.message}: {self.detail}"
|
{subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing/utils/pandas_integration.py
RENAMED
|
@@ -14,7 +14,7 @@ def batch_response_to_dataframe(response: dict):
|
|
|
14
14
|
import pandas as pd
|
|
15
15
|
|
|
16
16
|
rows = []
|
|
17
|
-
for entity in response.get("entities"
|
|
17
|
+
for entity in response.get("entities") or []:
|
|
18
18
|
row = {"entity_id": entity.get("_entity_id", "")}
|
|
19
19
|
fv = entity.get("final_values", {})
|
|
20
20
|
for k, v in fv.items():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subspacecomputing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for Subspace Computing Engine API (by Beausoft)
|
|
5
5
|
Home-page: https://www.subspacecomputing.com/developer
|
|
6
6
|
Author: Beausoft
|
|
@@ -49,6 +49,17 @@ bsce = BSCE(api_key='your-api-key-here')
|
|
|
49
49
|
# bsce = BSCE(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Teams (`X-Team-Id`)
|
|
53
|
+
|
|
54
|
+
Keys can carry a `default_team_id` from the portal. If the user is in **several** teams, some endpoints require an explicit team: pass `team_id=...` or call `set_team_id` so the SDK sends `X-Team-Id`.
|
|
55
|
+
|
|
56
|
+
If you omit it when required, the API returns **400** with `error.reason` **`team_context_required`**. Use `ValidationError` and read `exc.reason`.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
bsce = BSCE(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
60
|
+
bsce.set_team_id(None) # clear override for following requests
|
|
61
|
+
```
|
|
62
|
+
|
|
52
63
|
### Simple Projection (1 scenario)
|
|
53
64
|
|
|
54
65
|
```python
|
|
@@ -245,7 +256,7 @@ if quota:
|
|
|
245
256
|
|
|
246
257
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
247
258
|
|
|
248
|
-
API reference is available at https://
|
|
259
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
249
260
|
|
|
250
261
|
For support, reach out to contact@beausoft.ca
|
|
251
262
|
|
|
@@ -331,3 +331,50 @@ class TestBatchParameters:
|
|
|
331
331
|
assert 'aggregations' in call_args[1]['json']
|
|
332
332
|
assert result['aggregations']['capital_total'] == 3000.0
|
|
333
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
|
+
bsce = BSCE(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
|
+
bsce.last_response = mock_response
|
|
344
|
+
|
|
345
|
+
result = bsce.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
|
+
bsce = BSCE(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
|
+
bsce.last_response = mock_response
|
|
359
|
+
|
|
360
|
+
result = bsce.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
|
+
bsce = BSCE(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
|
+
bsce.last_response = mock_response
|
|
375
|
+
|
|
376
|
+
result = bsce.get_quota_info()
|
|
377
|
+
assert result["limit"] is None
|
|
378
|
+
assert result["remaining"] is None
|
|
379
|
+
assert result["used"] == 50
|
|
380
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
File without changes
|
{subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.0 → subspacecomputing-0.1.2}/subspacecomputing.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|