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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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