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.
Files changed (20) hide show
  1. {subspacecomputing-0.1.0/subspacecomputing.egg-info → subspacecomputing-0.1.1}/PKG-INFO +2 -2
  2. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/README.md +1 -1
  3. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/pyproject.toml +1 -1
  4. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/setup.py +1 -1
  5. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/__init__.py +1 -1
  6. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/client.py +31 -17
  7. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/errors.py +1 -1
  8. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/utils/pandas_integration.py +1 -1
  9. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1/subspacecomputing.egg-info}/PKG-INFO +2 -2
  10. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/SOURCES.txt +2 -1
  11. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/tests/test_client_unit.py +47 -0
  12. subspacecomputing-0.1.1/tests/test_pandas_integration.py +41 -0
  13. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/LICENSE +0 -0
  14. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/MANIFEST.in +0 -0
  15. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/setup.cfg +0 -0
  16. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing/utils/__init__.py +0 -0
  17. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/dependency_links.txt +0 -0
  18. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/requires.txt +0 -0
  19. {subspacecomputing-0.1.0 → subspacecomputing-0.1.1}/subspacecomputing.egg-info/top_level.txt +0 -0
  20. {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.0
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://api.subspacecomputing.com/docs
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://api.subspacecomputing.com/docs
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.0"
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.0",
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",
@@ -13,7 +13,7 @@ from .errors import (
13
13
  ValidationError,
14
14
  )
15
15
 
16
- __version__ = "0.1.0"
16
+ __version__ = "0.1.1"
17
17
  __all__ = [
18
18
  "BSCE",
19
19
  "BSCEError",
@@ -140,15 +140,15 @@ class BSCE:
140
140
  scenario_id: int,
141
141
  ) -> Dict[str, Any]:
142
142
  """
143
- Rejoue un scénario spécifique d'une simulation précédente.
143
+ Replay a specific scenario from a previous simulation.
144
144
 
145
145
  Args:
146
- original_spec: SP Model complet de la simulation initiale
147
- original_seeds: Objet seeds de la réponse de simulate() (seeds.global, seeds.scenarios)
148
- scenario_id: Index du scénario à rejouer (0 à scenarios-1)
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
- Réponse simulate pour ce scénario unique (final_values, sample_path_s0, etc.)
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
- Rejoue un scénario à partir d'une run stockée (nécessite seeds persistés).
404
+ Replay a scenario from a stored run (requires persisted seeds).
405
405
 
406
406
  Args:
407
- run_id: ID de la run (créée avec meta.store_seeds_for_replay: true)
408
- scenario_id: Index du scénario à rejouer (0 à scenarios-1)
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
- Réponse simulate pour ce scénario unique (final_values, sample_path_s0, etc.)
411
+ Simulate response for this single scenario (final_values, sample_path_s0, etc.)
412
412
 
413
413
  Raises:
414
- ValidationError: Si seeds non stockés ou scenario_id hors limites
415
- AuthenticationError: Si clé API invalide
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) # avec store_seeds_for_replay: true
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": int(limit) if limit else None,
464
- "remaining": int(remaining) if remaining else None,
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": int(limit) if limit and limit != "unlimited" else None,
483
- "remaining": int(remaining) if remaining else None,
484
- "used": int(used) if used else None,
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 (ValueError, KeyError):
24
+ except ValueError:
25
25
  self.detail = response.text
26
26
 
27
27
  super().__init__(self.message)
@@ -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.0
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://api.subspacecomputing.com/docs
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
 
@@ -14,4 +14,5 @@ subspacecomputing.egg-info/top_level.txt
14
14
  subspacecomputing/utils/__init__.py
15
15
  subspacecomputing/utils/pandas_integration.py
16
16
  tests/test_client_integration.py
17
- tests/test_client_unit.py
17
+ tests/test_client_unit.py
18
+ tests/test_pandas_integration.py
@@ -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]