subspacecomputing 0.1.0__py3-none-any.whl
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/__init__.py +24 -0
- subspacecomputing/client.py +485 -0
- subspacecomputing/errors.py +56 -0
- subspacecomputing/utils/__init__.py +1 -0
- subspacecomputing/utils/pandas_integration.py +23 -0
- subspacecomputing-0.1.0.dist-info/METADATA +258 -0
- subspacecomputing-0.1.0.dist-info/RECORD +10 -0
- subspacecomputing-0.1.0.dist-info/WHEEL +5 -0
- subspacecomputing-0.1.0.dist-info/licenses/LICENSE +21 -0
- subspacecomputing-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subspace Computing Engine - Python SDK
|
|
3
|
+
|
|
4
|
+
Python SDK for the Subspace Computing Engine API (by Beausoft).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import BSCE
|
|
8
|
+
from .errors import (
|
|
9
|
+
BSCEError,
|
|
10
|
+
QuotaExceededError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BSCE",
|
|
19
|
+
"BSCEError",
|
|
20
|
+
"QuotaExceededError",
|
|
21
|
+
"RateLimitError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"ValidationError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python client for Subspace Computing Engine API.
|
|
3
|
+
|
|
4
|
+
This module provides a simple Python interface to interact with the Subspace Computing API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from .errors import (
|
|
10
|
+
BSCEError,
|
|
11
|
+
QuotaExceededError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Default timeout for HTTP requests (seconds)
|
|
18
|
+
DEFAULT_TIMEOUT = 60
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BSCE:
|
|
22
|
+
"""Python client for Subspace Computing Engine API."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_key: str,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
timeout: Optional[int] = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the BSCE client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
api_key: Your API key (format: be_live_... or sk_pro_...).
|
|
35
|
+
Get one at https://subspacecomputing.com or via the portal.
|
|
36
|
+
base_url: API base URL (optional, defaults to https://api.subspacecomputing.com).
|
|
37
|
+
Only useful for local testing or custom environments.
|
|
38
|
+
timeout: Request timeout in seconds (optional, defaults to 60).
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If api_key is empty or None.
|
|
42
|
+
"""
|
|
43
|
+
if not api_key or not str(api_key).strip():
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"API key is required. Get one at https://subspacecomputing.com or via the portal."
|
|
46
|
+
)
|
|
47
|
+
self.api_key = api_key.strip()
|
|
48
|
+
self.base_url = (base_url or "https://api.subspacecomputing.com").rstrip("/")
|
|
49
|
+
self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
50
|
+
self.session = requests.Session()
|
|
51
|
+
self.session.headers.update({
|
|
52
|
+
"X-API-Key": self.api_key,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
})
|
|
55
|
+
self.last_response = None # Store last response for header access
|
|
56
|
+
|
|
57
|
+
def _handle_response(self, response: requests.Response):
|
|
58
|
+
"""
|
|
59
|
+
Handle HTTP errors and raise appropriate exceptions.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
response: requests Response object
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
RateLimitError: If rate limit exceeded (429)
|
|
66
|
+
QuotaExceededError: If monthly quota exceeded (429)
|
|
67
|
+
AuthenticationError: If API key invalid (401)
|
|
68
|
+
ValidationError: If validation error (400)
|
|
69
|
+
BSCEError: For other errors
|
|
70
|
+
"""
|
|
71
|
+
if response.status_code == 429:
|
|
72
|
+
# Check if it's rate limit or quota
|
|
73
|
+
error_text = response.text.lower()
|
|
74
|
+
if "rate limit" in error_text or "retry-after" in response.headers:
|
|
75
|
+
raise RateLimitError("Rate limit exceeded", response)
|
|
76
|
+
else:
|
|
77
|
+
raise QuotaExceededError("Monthly quota exceeded", response)
|
|
78
|
+
elif response.status_code == 401:
|
|
79
|
+
raise AuthenticationError(
|
|
80
|
+
"Invalid or missing API key. Get one at https://subspacecomputing.com",
|
|
81
|
+
response,
|
|
82
|
+
)
|
|
83
|
+
elif response.status_code == 403:
|
|
84
|
+
raise BSCEError("Access denied", response)
|
|
85
|
+
elif response.status_code == 400:
|
|
86
|
+
raise ValidationError("Validation error", response)
|
|
87
|
+
elif not response.ok:
|
|
88
|
+
raise BSCEError(f"API error: {response.status_code}", response)
|
|
89
|
+
|
|
90
|
+
# Main endpoints (calculations)
|
|
91
|
+
|
|
92
|
+
def project(self, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Run a single projection (1 scenario).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
spec: SP Model (must have scenarios=1)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Response with trajectory and final_values
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValidationError: If spec is invalid
|
|
104
|
+
QuotaExceededError: If quota exceeded
|
|
105
|
+
RateLimitError: If rate limit exceeded
|
|
106
|
+
"""
|
|
107
|
+
response = self.session.post(
|
|
108
|
+
f"{self.base_url}/project", json=spec, timeout=self.timeout
|
|
109
|
+
)
|
|
110
|
+
self.last_response = response
|
|
111
|
+
self._handle_response(response)
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
def simulate(self, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
115
|
+
"""
|
|
116
|
+
Run a Monte Carlo simulation (multiple scenarios).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
spec: SP Model with scenarios >= 1
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Response with statistics, final_values, sample_path_s0
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValidationError: If spec is invalid
|
|
126
|
+
QuotaExceededError: If quota exceeded
|
|
127
|
+
RateLimitError: If rate limit exceeded
|
|
128
|
+
"""
|
|
129
|
+
response = self.session.post(
|
|
130
|
+
f"{self.base_url}/simulate", json=spec, timeout=self.timeout
|
|
131
|
+
)
|
|
132
|
+
self.last_response = response
|
|
133
|
+
self._handle_response(response)
|
|
134
|
+
return response.json()
|
|
135
|
+
|
|
136
|
+
def replay_scenario(
|
|
137
|
+
self,
|
|
138
|
+
original_spec: Dict[str, Any],
|
|
139
|
+
original_seeds: Dict[str, Any],
|
|
140
|
+
scenario_id: int,
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Rejoue un scénario spécifique d'une simulation précédente.
|
|
144
|
+
|
|
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)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Réponse simulate pour ce scénario unique (final_values, sample_path_s0, etc.)
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
result = client.simulate(spec)
|
|
155
|
+
replay = client.replay_scenario(spec, result["seeds"], scenario_id=12)
|
|
156
|
+
"""
|
|
157
|
+
payload = {
|
|
158
|
+
"original_spec": original_spec,
|
|
159
|
+
"original_seeds": original_seeds,
|
|
160
|
+
}
|
|
161
|
+
response = self.session.post(
|
|
162
|
+
f"{self.base_url}/simulate/scenario/{scenario_id}",
|
|
163
|
+
json=payload,
|
|
164
|
+
timeout=self.timeout,
|
|
165
|
+
)
|
|
166
|
+
self.last_response = response
|
|
167
|
+
self._handle_response(response)
|
|
168
|
+
return response.json()
|
|
169
|
+
|
|
170
|
+
def project_batch(
|
|
171
|
+
self,
|
|
172
|
+
template: Dict[str, Any],
|
|
173
|
+
batch_params: List[Dict[str, Any]],
|
|
174
|
+
aggregations: Optional[List[Dict[str, Any]]] = None,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""
|
|
177
|
+
Run a batch projection (multiple entities).
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
template: SP Model template with batch_params.xxx references
|
|
181
|
+
batch_params: List of entities with their parameters
|
|
182
|
+
aggregations: List of global aggregations (optional)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Response with entities (results per entity) and aggregations
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValidationError: If spec is invalid
|
|
189
|
+
QuotaExceededError: If quota exceeded
|
|
190
|
+
RateLimitError: If rate limit exceeded
|
|
191
|
+
"""
|
|
192
|
+
spec = {
|
|
193
|
+
"template": template,
|
|
194
|
+
"batch_params": batch_params,
|
|
195
|
+
}
|
|
196
|
+
if aggregations:
|
|
197
|
+
spec["aggregations"] = aggregations
|
|
198
|
+
|
|
199
|
+
response = self.session.post(
|
|
200
|
+
f"{self.base_url}/project/batch", json=spec, timeout=self.timeout
|
|
201
|
+
)
|
|
202
|
+
self.last_response = response
|
|
203
|
+
self._handle_response(response)
|
|
204
|
+
return response.json()
|
|
205
|
+
|
|
206
|
+
# Utility endpoints
|
|
207
|
+
|
|
208
|
+
def validate(self, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Validate an SP Model without executing it.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
spec: SP Model or BatchSpec to validate
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Response with is_valid, warnings, errors
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValidationError: If validation error
|
|
220
|
+
"""
|
|
221
|
+
response = self.session.post(
|
|
222
|
+
f"{self.base_url}/validate", json=spec, timeout=self.timeout
|
|
223
|
+
)
|
|
224
|
+
self.last_response = response
|
|
225
|
+
self._handle_response(response)
|
|
226
|
+
return response.json()
|
|
227
|
+
|
|
228
|
+
def get_examples(self) -> Dict[str, Any]:
|
|
229
|
+
"""
|
|
230
|
+
Get ready-to-use SP Model examples.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Response with list of examples
|
|
234
|
+
|
|
235
|
+
Note:
|
|
236
|
+
Public endpoint (no API key required)
|
|
237
|
+
"""
|
|
238
|
+
response = self.session.get(
|
|
239
|
+
f"{self.base_url}/examples", timeout=self.timeout
|
|
240
|
+
)
|
|
241
|
+
self.last_response = response
|
|
242
|
+
self._handle_response(response)
|
|
243
|
+
return response.json()
|
|
244
|
+
|
|
245
|
+
def get_usage(self) -> Dict[str, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Get user usage and quotas.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Response with usage, quotas, plan info
|
|
251
|
+
"""
|
|
252
|
+
response = self.session.get(
|
|
253
|
+
f"{self.base_url}/usage", timeout=self.timeout
|
|
254
|
+
)
|
|
255
|
+
self.last_response = response
|
|
256
|
+
self._handle_response(response)
|
|
257
|
+
return response.json()
|
|
258
|
+
|
|
259
|
+
def get_plans(self) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Get list of available plans.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Response with list of plans
|
|
265
|
+
|
|
266
|
+
Note:
|
|
267
|
+
Public endpoint (no API key required)
|
|
268
|
+
"""
|
|
269
|
+
response = self.session.get(
|
|
270
|
+
f"{self.base_url}/plans", timeout=self.timeout
|
|
271
|
+
)
|
|
272
|
+
self.last_response = response
|
|
273
|
+
self._handle_response(response)
|
|
274
|
+
return response.json()
|
|
275
|
+
|
|
276
|
+
# CRUD endpoints (projections)
|
|
277
|
+
|
|
278
|
+
def list_projections(
|
|
279
|
+
self,
|
|
280
|
+
limit: Optional[int] = None,
|
|
281
|
+
offset: Optional[int] = None,
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
List saved projections.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
limit: Maximum number of results (optional)
|
|
288
|
+
offset: Offset for pagination (optional)
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Response with list of projections
|
|
292
|
+
"""
|
|
293
|
+
params = {}
|
|
294
|
+
if limit is not None:
|
|
295
|
+
params["limit"] = limit
|
|
296
|
+
if offset is not None:
|
|
297
|
+
params["offset"] = offset
|
|
298
|
+
|
|
299
|
+
response = self.session.get(
|
|
300
|
+
f"{self.base_url}/projections",
|
|
301
|
+
params=params,
|
|
302
|
+
timeout=self.timeout,
|
|
303
|
+
)
|
|
304
|
+
self.last_response = response
|
|
305
|
+
self._handle_response(response)
|
|
306
|
+
return response.json()
|
|
307
|
+
|
|
308
|
+
def get_projection(self, projection_id: str) -> Dict[str, Any]:
|
|
309
|
+
"""
|
|
310
|
+
Get a projection by ID.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
projection_id: Projection ID
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Response with projection details
|
|
317
|
+
"""
|
|
318
|
+
response = self.session.get(
|
|
319
|
+
f"{self.base_url}/projections/{projection_id}",
|
|
320
|
+
timeout=self.timeout,
|
|
321
|
+
)
|
|
322
|
+
self.last_response = response
|
|
323
|
+
self._handle_response(response)
|
|
324
|
+
return response.json()
|
|
325
|
+
|
|
326
|
+
def delete_projection(self, projection_id: str) -> None:
|
|
327
|
+
"""
|
|
328
|
+
Delete a projection.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
projection_id: Projection ID
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
BSCEError: If error during deletion
|
|
335
|
+
"""
|
|
336
|
+
response = self.session.delete(
|
|
337
|
+
f"{self.base_url}/projections/{projection_id}",
|
|
338
|
+
timeout=self.timeout,
|
|
339
|
+
)
|
|
340
|
+
self.last_response = response
|
|
341
|
+
self._handle_response(response)
|
|
342
|
+
# 204 No Content, no body
|
|
343
|
+
|
|
344
|
+
# CRUD endpoints (runs)
|
|
345
|
+
|
|
346
|
+
def list_runs(
|
|
347
|
+
self,
|
|
348
|
+
projection_id: Optional[str] = None,
|
|
349
|
+
parent_batch_run_id: Optional[str] = None,
|
|
350
|
+
limit: Optional[int] = None,
|
|
351
|
+
offset: Optional[int] = None,
|
|
352
|
+
) -> Dict[str, Any]:
|
|
353
|
+
"""
|
|
354
|
+
List saved runs.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
projection_id: Filter by projection_id (optional)
|
|
358
|
+
parent_batch_run_id: Filter by parent batch run (optional)
|
|
359
|
+
limit: Maximum number of results (optional)
|
|
360
|
+
offset: Offset for pagination (optional)
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Response with list of runs
|
|
364
|
+
"""
|
|
365
|
+
params = {}
|
|
366
|
+
if projection_id:
|
|
367
|
+
params["projection_id"] = projection_id
|
|
368
|
+
if parent_batch_run_id:
|
|
369
|
+
params["parent_batch_run_id"] = parent_batch_run_id
|
|
370
|
+
if limit is not None:
|
|
371
|
+
params["limit"] = limit
|
|
372
|
+
if offset is not None:
|
|
373
|
+
params["offset"] = offset
|
|
374
|
+
|
|
375
|
+
response = self.session.get(
|
|
376
|
+
f"{self.base_url}/projection-runs",
|
|
377
|
+
params=params,
|
|
378
|
+
timeout=self.timeout,
|
|
379
|
+
)
|
|
380
|
+
self.last_response = response
|
|
381
|
+
self._handle_response(response)
|
|
382
|
+
return response.json()
|
|
383
|
+
|
|
384
|
+
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
385
|
+
"""
|
|
386
|
+
Get a run by ID.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
run_id: Run ID
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Response with run details
|
|
393
|
+
"""
|
|
394
|
+
response = self.session.get(
|
|
395
|
+
f"{self.base_url}/projection-runs/{run_id}",
|
|
396
|
+
timeout=self.timeout,
|
|
397
|
+
)
|
|
398
|
+
self.last_response = response
|
|
399
|
+
self._handle_response(response)
|
|
400
|
+
return response.json()
|
|
401
|
+
|
|
402
|
+
def replay_run(self, run_id: str, scenario_id: int) -> Dict[str, Any]:
|
|
403
|
+
"""
|
|
404
|
+
Rejoue un scénario à partir d'une run stockée (nécessite seeds persistés).
|
|
405
|
+
|
|
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)
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Réponse simulate pour ce scénario unique (final_values, sample_path_s0, etc.)
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
ValidationError: Si seeds non stockés ou scenario_id hors limites
|
|
415
|
+
AuthenticationError: Si clé API invalide
|
|
416
|
+
|
|
417
|
+
Example:
|
|
418
|
+
result = client.simulate(spec) # avec store_seeds_for_replay: true
|
|
419
|
+
replay = client.replay_run(result["run_id"], scenario_id=12)
|
|
420
|
+
"""
|
|
421
|
+
response = self.session.post(
|
|
422
|
+
f"{self.base_url}/projection-runs/{run_id}/replay/{scenario_id}",
|
|
423
|
+
timeout=self.timeout,
|
|
424
|
+
)
|
|
425
|
+
self.last_response = response
|
|
426
|
+
self._handle_response(response)
|
|
427
|
+
return response.json()
|
|
428
|
+
|
|
429
|
+
def delete_run(self, run_id: str) -> None:
|
|
430
|
+
"""
|
|
431
|
+
Delete a run.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
run_id: Run ID
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
BSCEError: If error during deletion
|
|
438
|
+
"""
|
|
439
|
+
response = self.session.delete(
|
|
440
|
+
f"{self.base_url}/projection-runs/{run_id}",
|
|
441
|
+
timeout=self.timeout,
|
|
442
|
+
)
|
|
443
|
+
self.last_response = response
|
|
444
|
+
self._handle_response(response)
|
|
445
|
+
# 204 No Content, no body
|
|
446
|
+
|
|
447
|
+
# Helper methods for accessing rate limit and quota info
|
|
448
|
+
|
|
449
|
+
def get_rate_limit_info(self) -> Optional[Dict[str, Any]]:
|
|
450
|
+
"""
|
|
451
|
+
Get rate limit information from the last response.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Dict with 'limit' and 'remaining' keys, or None if no response yet
|
|
455
|
+
"""
|
|
456
|
+
if not self.last_response:
|
|
457
|
+
return None
|
|
458
|
+
limit = self.last_response.headers.get("X-RateLimit-Limit")
|
|
459
|
+
remaining = self.last_response.headers.get("X-RateLimit-Remaining")
|
|
460
|
+
if limit is None and remaining is None:
|
|
461
|
+
return None
|
|
462
|
+
return {
|
|
463
|
+
"limit": int(limit) if limit else None,
|
|
464
|
+
"remaining": int(remaining) if remaining else None,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
def get_quota_info(self) -> Optional[Dict[str, Any]]:
|
|
468
|
+
"""
|
|
469
|
+
Get quota information from the last response.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Dict with 'limit', 'remaining', and 'used' keys, or None if no response yet
|
|
473
|
+
"""
|
|
474
|
+
if not self.last_response:
|
|
475
|
+
return None
|
|
476
|
+
limit = self.last_response.headers.get("X-Quota-Limit")
|
|
477
|
+
remaining = self.last_response.headers.get("X-Quota-Remaining")
|
|
478
|
+
used = self.last_response.headers.get("X-Quota-Used")
|
|
479
|
+
if limit is None and remaining is None and used is None:
|
|
480
|
+
return None
|
|
481
|
+
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,
|
|
485
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the Subspace Computing SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BSCEError(Exception):
|
|
10
|
+
"""Base error for the Subspace Computing SDK."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, response: Optional[requests.Response] = None):
|
|
13
|
+
self.message = message
|
|
14
|
+
self.response = response
|
|
15
|
+
self.status_code = response.status_code if response else None
|
|
16
|
+
self.detail = None
|
|
17
|
+
|
|
18
|
+
# Try to extract error details from the response
|
|
19
|
+
if response is not None:
|
|
20
|
+
try:
|
|
21
|
+
error_data = response.json()
|
|
22
|
+
if isinstance(error_data, dict):
|
|
23
|
+
self.detail = error_data.get("detail", error_data.get("message"))
|
|
24
|
+
except (ValueError, KeyError):
|
|
25
|
+
self.detail = response.text
|
|
26
|
+
|
|
27
|
+
super().__init__(self.message)
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
if self.detail:
|
|
31
|
+
return f"{self.message}: {self.detail}"
|
|
32
|
+
return self.message
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class QuotaExceededError(BSCEError):
|
|
36
|
+
"""Monthly quota exceeded (429)."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RateLimitError(BSCEError):
|
|
42
|
+
"""Rate limit exceeded (429)."""
|
|
43
|
+
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AuthenticationError(BSCEError):
|
|
48
|
+
"""Invalid or missing API key (401)."""
|
|
49
|
+
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ValidationError(BSCEError):
|
|
54
|
+
"""SP Model validation error (400)."""
|
|
55
|
+
|
|
56
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utilities for Subspace Computing Engine SDK."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pandas integration utilities for batch responses.
|
|
3
|
+
|
|
4
|
+
Requires: pip install pandas
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def batch_response_to_dataframe(response: dict):
|
|
9
|
+
"""
|
|
10
|
+
Convert a project_batch or simulate_batch response to a pandas DataFrame.
|
|
11
|
+
|
|
12
|
+
Each entity becomes one row. Columns include entity_id plus all final_values.
|
|
13
|
+
"""
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
rows = []
|
|
17
|
+
for entity in response.get("entities", []):
|
|
18
|
+
row = {"entity_id": entity.get("_entity_id", "")}
|
|
19
|
+
fv = entity.get("final_values", {})
|
|
20
|
+
for k, v in fv.items():
|
|
21
|
+
row[k] = v
|
|
22
|
+
rows.append(row)
|
|
23
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: subspacecomputing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Subspace Computing Engine API (by Beausoft)
|
|
5
|
+
Home-page: https://www.subspacecomputing.com/developer
|
|
6
|
+
Author: Beausoft
|
|
7
|
+
Author-email: Beausoft <contact@beausoft.ca>
|
|
8
|
+
Project-URL: Documentation, https://www.subspacecomputing.com/developer
|
|
9
|
+
Project-URL: Homepage, https://www.subspacecomputing.com/developer
|
|
10
|
+
Project-URL: Support, https://www.subspacecomputing.com/developer
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.31.0
|
|
15
|
+
Provides-Extra: pandas
|
|
16
|
+
Requires-Dist: pandas>=2.0.0; extra == "pandas"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
22
|
+
Requires-Dist: fastapi>=0.100.0; extra == "dev"
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
|
|
28
|
+
# Subspace Computing Engine - Python SDK
|
|
29
|
+
|
|
30
|
+
Python SDK for the Subspace Computing Engine API (by Beausoft).
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install subspacecomputing
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Initialization
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from subspacecomputing import BSCE
|
|
44
|
+
|
|
45
|
+
# Initialize the client (defaults to production URL)
|
|
46
|
+
bsce = BSCE(api_key='your-api-key-here')
|
|
47
|
+
|
|
48
|
+
# For local testing or custom environments (optional)
|
|
49
|
+
# bsce = BSCE(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Simple Projection (1 scenario)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Create a simple SP Model
|
|
56
|
+
spec = {
|
|
57
|
+
'scenarios': 1, # Must be 1 for /project
|
|
58
|
+
'steps': 12,
|
|
59
|
+
'variables': [
|
|
60
|
+
{
|
|
61
|
+
'name': 'capital',
|
|
62
|
+
'init': 1000.0,
|
|
63
|
+
'formula': 'capital[t-1] * 1.05' # 5% growth per period
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Run the projection
|
|
69
|
+
result = bsce.project(spec)
|
|
70
|
+
|
|
71
|
+
# Display results
|
|
72
|
+
print(f"Final capital: {result['final_values']['capital']}")
|
|
73
|
+
print(f"Trajectory: {result['trajectory']['capital']}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Monte Carlo Simulation (Multiple scenarios)
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# SP Model with random variables
|
|
80
|
+
spec = {
|
|
81
|
+
'scenarios': 1000, # 1000 Monte Carlo scenarios
|
|
82
|
+
'steps': 12,
|
|
83
|
+
'variables': [
|
|
84
|
+
{
|
|
85
|
+
'name': 'taux',
|
|
86
|
+
'dist': 'uniform',
|
|
87
|
+
'params': {'min': 0.03, 'max': 0.07},
|
|
88
|
+
'per': 'scenario'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
'name': 'capital',
|
|
92
|
+
'init': 1000.0,
|
|
93
|
+
'formula': 'capital[t-1] * (1 + taux)'
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Run the simulation
|
|
99
|
+
result = bsce.simulate(spec)
|
|
100
|
+
|
|
101
|
+
# Analyze results
|
|
102
|
+
print(f"Mean final capital: {result['last_mean']['capital']}")
|
|
103
|
+
print(f"Median: {result['statistics']['capital']['median']}")
|
|
104
|
+
print(f"P5: {result['statistics']['capital']['percentiles']['5']}")
|
|
105
|
+
print(f"P95: {result['statistics']['capital']['percentiles']['95']}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Batch Mode (Multiple Entities)
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# SP Model template
|
|
112
|
+
template = {
|
|
113
|
+
'scenarios': 1,
|
|
114
|
+
'steps': '65 - batch_params.age', # Dynamic steps
|
|
115
|
+
'variables': [
|
|
116
|
+
{
|
|
117
|
+
'name': 'age_actuel',
|
|
118
|
+
'init': 'batch_params.age'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
'name': 'salaire',
|
|
122
|
+
'init': 'batch_params.salary',
|
|
123
|
+
'formula': 'salaire[t-1] * 1.03' # 3% annual increase
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
'name': 'capital_retraite',
|
|
127
|
+
'init': 0.0,
|
|
128
|
+
'formula': 'capital_retraite[t-1] * 1.05 + salaire[t] * 0.10'
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Entity data
|
|
134
|
+
batch_params = [
|
|
135
|
+
{'entity_id': 'emp_001', 'age': 45, 'salary': 60000},
|
|
136
|
+
{'entity_id': 'emp_002', 'age': 50, 'salary': 80000},
|
|
137
|
+
{'entity_id': 'emp_003', 'age': 35, 'salary': 50000}
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
# Global aggregations (optional)
|
|
141
|
+
aggregations = [
|
|
142
|
+
{
|
|
143
|
+
'name': 'capital_total',
|
|
144
|
+
'formula': 'sum(capital_retraite[t_final])'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
'name': 'moyenne_capital',
|
|
148
|
+
'formula': 'mean(capital_retraite[t_final])'
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
# Run batch
|
|
153
|
+
result = bsce.project_batch(
|
|
154
|
+
template=template,
|
|
155
|
+
batch_params=batch_params,
|
|
156
|
+
aggregations=aggregations
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Analyze results
|
|
160
|
+
for entity in result['entities']:
|
|
161
|
+
print(f"{entity['_entity_id']}: Capital = {entity['final_values']['capital_retraite']}")
|
|
162
|
+
|
|
163
|
+
print(f"Total capital: {result['aggregations']['capital_total']}")
|
|
164
|
+
print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Validation
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Validate an SP Model before execution
|
|
171
|
+
validation = bsce.validate(spec)
|
|
172
|
+
|
|
173
|
+
if validation['is_valid']:
|
|
174
|
+
print("✅ SP Model is valid")
|
|
175
|
+
if validation.get('warnings'):
|
|
176
|
+
print(f"⚠️ Warnings: {validation['warnings']}")
|
|
177
|
+
else:
|
|
178
|
+
print(f"❌ Errors: {validation['errors']}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Utilities
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# Get examples
|
|
185
|
+
examples = bsce.get_examples()
|
|
186
|
+
print(f"Available examples: {len(examples['examples'])}")
|
|
187
|
+
|
|
188
|
+
# Check usage
|
|
189
|
+
usage = bsce.get_usage()
|
|
190
|
+
print(f"Simulations used: {usage['usage']['simulations_used']}/{usage['usage']['simulations_limit']}")
|
|
191
|
+
|
|
192
|
+
# Get plans
|
|
193
|
+
plans = bsce.get_plans()
|
|
194
|
+
for plan in plans['plans']:
|
|
195
|
+
print(f"{plan['name']}: ${plan['price_monthly']}/month")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Error Handling
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from subspacecomputing import (
|
|
202
|
+
BSCE,
|
|
203
|
+
QuotaExceededError,
|
|
204
|
+
RateLimitError,
|
|
205
|
+
AuthenticationError,
|
|
206
|
+
ValidationError,
|
|
207
|
+
BSCEError
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
result = bsce.simulate(spec)
|
|
212
|
+
except QuotaExceededError as e:
|
|
213
|
+
print(f"Monthly quota exceeded: {e}")
|
|
214
|
+
except RateLimitError as e:
|
|
215
|
+
print(f"Rate limit exceeded: {e}")
|
|
216
|
+
print(f"Retry after: {e.response.headers.get('Retry-After')} seconds")
|
|
217
|
+
except AuthenticationError as e:
|
|
218
|
+
print(f"Invalid API key: {e}")
|
|
219
|
+
except ValidationError as e:
|
|
220
|
+
print(f"Validation error: {e.detail}")
|
|
221
|
+
except BSCEError as e:
|
|
222
|
+
print(f"API error: {e}")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Rate Limit and Quota Information
|
|
226
|
+
|
|
227
|
+
After making a request, you can check your rate limit and quota status:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# Make a request
|
|
231
|
+
result = bsce.project(spec)
|
|
232
|
+
|
|
233
|
+
# Check rate limit info
|
|
234
|
+
rate_limit = bsce.get_rate_limit_info()
|
|
235
|
+
if rate_limit:
|
|
236
|
+
print(f"Rate limit: {rate_limit['remaining']}/{rate_limit['limit']} remaining")
|
|
237
|
+
|
|
238
|
+
# Check quota info
|
|
239
|
+
quota = bsce.get_quota_info()
|
|
240
|
+
if quota:
|
|
241
|
+
print(f"Quota: {quota['used']}/{quota['limit']} used, {quota['remaining']} remaining")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Documentation
|
|
245
|
+
|
|
246
|
+
Check out the full documentation at https://www.subspacecomputing.com/developer
|
|
247
|
+
|
|
248
|
+
API reference is available at https://api.subspacecomputing.com/docs
|
|
249
|
+
|
|
250
|
+
For support, reach out to contact@beausoft.ca
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT License. Check the LICENSE file for details.
|
|
255
|
+
|
|
256
|
+
## Copyright
|
|
257
|
+
|
|
258
|
+
© 2025 Beausoft Inc. All Rights Reserved
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
subspacecomputing/__init__.py,sha256=cyvLCoJH1OXc-UeNdcEEFElhXzYPL-3EJsA438yUkrQ,431
|
|
2
|
+
subspacecomputing/client.py,sha256=kWeb0CVARQYOdwIw-EzLodNncOiCiwDmv0Xm4MstQQ4,15372
|
|
3
|
+
subspacecomputing/errors.py,sha256=NJ0KE1IAYYrXwB-e_zSh4AYP0A8XUNvEDQI2JF7jcSg,1329
|
|
4
|
+
subspacecomputing/utils/__init__.py,sha256=A0fX-wEb8uDR8NktIUvJrmRapo6Chj8W-aXk42cI9DM,51
|
|
5
|
+
subspacecomputing/utils/pandas_integration.py,sha256=3PJapJ34oacAszZFZgiIxfzmT-E3KG3rkRMNOGMgEPg,615
|
|
6
|
+
subspacecomputing-0.1.0.dist-info/licenses/LICENSE,sha256=74YyzYmjEMtAPgWKqqEond9N8WeRZWYNwHjBi4pvoMM,1070
|
|
7
|
+
subspacecomputing-0.1.0.dist-info/METADATA,sha256=ubDNgoN4gAnPmX5pF0iqTlzOXuUIu1PrViMvqlD-iqs,6420
|
|
8
|
+
subspacecomputing-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
subspacecomputing-0.1.0.dist-info/top_level.txt,sha256=GHDzXuS84ahs61dBpllwd_Jv1faPFqPftINXvX8u7Z0,18
|
|
10
|
+
subspacecomputing-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Beausoft Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
subspacecomputing
|