subspacecomputing 0.1.0__tar.gz → 0.1.1__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.1}/PKG-INFO +2 -2
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/README.md +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/pyproject.toml +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/setup.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/__init__.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/client.py +31 -17
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/errors.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/utils/pandas_integration.py +1 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1/subspacecomputing.egg-info}/PKG-INFO +2 -2
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/SOURCES.txt +2 -1
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/tests/test_client_unit.py +47 -0
- subspacecomputing-0.1.1/tests/test_pandas_integration.py +41 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/LICENSE +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/MANIFEST.in +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/setup.cfg +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/utils/__init__.py +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/dependency_links.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/requires.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/top_level.txt +0 -0
- {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/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.1
|
|
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
|
|
@@ -245,7 +245,7 @@ if quota:
|
|
|
245
245
|
|
|
246
246
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
247
247
|
|
|
248
|
-
API reference is available at https://
|
|
248
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
249
249
|
|
|
250
250
|
For support, reach out to contact@beausoft.ca
|
|
251
251
|
|
|
@@ -218,7 +218,7 @@ if quota:
|
|
|
218
218
|
|
|
219
219
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
220
220
|
|
|
221
|
-
API reference is available at https://
|
|
221
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
222
222
|
|
|
223
223
|
For support, reach out to contact@beausoft.ca
|
|
224
224
|
|
|
@@ -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.1"
|
|
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"
|
|
@@ -13,7 +13,7 @@ except FileNotFoundError:
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="subspacecomputing",
|
|
16
|
-
version="0.1.
|
|
16
|
+
version="0.1.1",
|
|
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",
|
|
@@ -140,15 +140,15 @@ class BSCE:
|
|
|
140
140
|
scenario_id: int,
|
|
141
141
|
) -> Dict[str, Any]:
|
|
142
142
|
"""
|
|
143
|
-
|
|
143
|
+
Replay a specific scenario from a previous simulation.
|
|
144
144
|
|
|
145
145
|
Args:
|
|
146
|
-
original_spec: SP Model
|
|
147
|
-
original_seeds:
|
|
148
|
-
scenario_id: Index
|
|
146
|
+
original_spec: Full SP Model from the initial simulation
|
|
147
|
+
original_seeds: Seeds object from simulate() response (seeds.global, seeds.scenarios)
|
|
148
|
+
scenario_id: Index of the scenario to replay (0 to scenarios-1)
|
|
149
149
|
|
|
150
150
|
Returns:
|
|
151
|
-
|
|
151
|
+
Simulate response for this single scenario (final_values, sample_path_s0, etc.)
|
|
152
152
|
|
|
153
153
|
Example:
|
|
154
154
|
result = client.simulate(spec)
|
|
@@ -401,23 +401,24 @@ class BSCE:
|
|
|
401
401
|
|
|
402
402
|
def replay_run(self, run_id: str, scenario_id: int) -> Dict[str, Any]:
|
|
403
403
|
"""
|
|
404
|
-
|
|
404
|
+
Replay a scenario from a stored run (requires persisted seeds).
|
|
405
405
|
|
|
406
406
|
Args:
|
|
407
|
-
run_id: ID
|
|
408
|
-
scenario_id: Index
|
|
407
|
+
run_id: Run ID (created with meta.store_seeds_for_replay: true)
|
|
408
|
+
scenario_id: Index of the scenario to replay (0 to scenarios-1)
|
|
409
409
|
|
|
410
410
|
Returns:
|
|
411
|
-
|
|
411
|
+
Simulate response for this single scenario (final_values, sample_path_s0, etc.)
|
|
412
412
|
|
|
413
413
|
Raises:
|
|
414
|
-
ValidationError:
|
|
415
|
-
AuthenticationError:
|
|
414
|
+
ValidationError: If seeds not stored or scenario_id out of range
|
|
415
|
+
AuthenticationError: If API key invalid
|
|
416
416
|
|
|
417
417
|
Example:
|
|
418
|
-
result = client.simulate(spec) #
|
|
418
|
+
result = client.simulate(spec) # with store_seeds_for_replay: true
|
|
419
419
|
replay = client.replay_run(result["run_id"], scenario_id=12)
|
|
420
420
|
"""
|
|
421
|
+
# API expects run_id and scenario_id in URL path; no request body required
|
|
421
422
|
response = self.session.post(
|
|
422
423
|
f"{self.base_url}/projection-runs/{run_id}/replay/{scenario_id}",
|
|
423
424
|
timeout=self.timeout,
|
|
@@ -446,6 +447,19 @@ class BSCE:
|
|
|
446
447
|
|
|
447
448
|
# Helper methods for accessing rate limit and quota info
|
|
448
449
|
|
|
450
|
+
@staticmethod
|
|
451
|
+
def _safe_int(val: Optional[str]) -> Optional[int]:
|
|
452
|
+
"""
|
|
453
|
+
Safely convert a header value to int.
|
|
454
|
+
Returns None for None, 'unlimited', or non-numeric strings.
|
|
455
|
+
"""
|
|
456
|
+
if val is None or val == "unlimited":
|
|
457
|
+
return None
|
|
458
|
+
try:
|
|
459
|
+
return int(val)
|
|
460
|
+
except (ValueError, TypeError):
|
|
461
|
+
return None
|
|
462
|
+
|
|
449
463
|
def get_rate_limit_info(self) -> Optional[Dict[str, Any]]:
|
|
450
464
|
"""
|
|
451
465
|
Get rate limit information from the last response.
|
|
@@ -460,8 +474,8 @@ class BSCE:
|
|
|
460
474
|
if limit is None and remaining is None:
|
|
461
475
|
return None
|
|
462
476
|
return {
|
|
463
|
-
"limit":
|
|
464
|
-
"remaining":
|
|
477
|
+
"limit": self._safe_int(limit),
|
|
478
|
+
"remaining": self._safe_int(remaining),
|
|
465
479
|
}
|
|
466
480
|
|
|
467
481
|
def get_quota_info(self) -> Optional[Dict[str, Any]]:
|
|
@@ -479,7 +493,7 @@ class BSCE:
|
|
|
479
493
|
if limit is None and remaining is None and used is None:
|
|
480
494
|
return None
|
|
481
495
|
return {
|
|
482
|
-
"limit":
|
|
483
|
-
"remaining":
|
|
484
|
-
"used":
|
|
496
|
+
"limit": self._safe_int(limit),
|
|
497
|
+
"remaining": self._safe_int(remaining),
|
|
498
|
+
"used": self._safe_int(used),
|
|
485
499
|
}
|
|
@@ -21,7 +21,7 @@ class BSCEError(Exception):
|
|
|
21
21
|
error_data = response.json()
|
|
22
22
|
if isinstance(error_data, dict):
|
|
23
23
|
self.detail = error_data.get("detail", error_data.get("message"))
|
|
24
|
-
except
|
|
24
|
+
except ValueError:
|
|
25
25
|
self.detail = response.text
|
|
26
26
|
|
|
27
27
|
super().__init__(self.message)
|
{subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/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.1
|
|
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
|
|
@@ -245,7 +245,7 @@ if quota:
|
|
|
245
245
|
|
|
246
246
|
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
247
247
|
|
|
248
|
-
API reference is available at https://
|
|
248
|
+
API reference is available at https://www.subspacecomputing.com/docs
|
|
249
249
|
|
|
250
250
|
For support, reach out to contact@beausoft.ca
|
|
251
251
|
|
|
@@ -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.1}/subspacecomputing.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|