subspacecomputing 0.1.1__tar.gz → 0.1.4__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.1/subspacecomputing.egg-info → subspacecomputing-0.1.4}/PKG-INFO +33 -21
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/README.md +28 -17
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/pyproject.toml +6 -4
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/setup.py +4 -4
- subspacecomputing-0.1.4/subspacecomputing/__init__.py +29 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing/client.py +49 -11
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing/errors.py +24 -8
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4/subspacecomputing.egg-info}/PKG-INFO +33 -21
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/requires.txt +1 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/tests/test_client_integration.py +2 -2
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/tests/test_client_unit.py +63 -63
- subspacecomputing-0.1.1/subspacecomputing/__init__.py +0 -24
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/LICENSE +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/MANIFEST.in +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/setup.cfg +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing/utils/__init__.py +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing/utils/pandas_integration.py +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/SOURCES.txt +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/dependency_links.txt +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/top_level.txt +0 -0
- {subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/tests/test_pandas_integration.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subspacecomputing
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Python SDK for Subspace Computing Engine
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Python SDK for ASTRIA — Subspace Computing Engine
|
|
5
5
|
Home-page: https://www.subspacecomputing.com/developer
|
|
6
|
-
Author:
|
|
7
|
-
Author-email:
|
|
6
|
+
Author: Subspace Computing
|
|
7
|
+
Author-email: Subspace Computing <contact@beausoft.ca>
|
|
8
8
|
Project-URL: Documentation, https://www.subspacecomputing.com/developer
|
|
9
9
|
Project-URL: Homepage, https://www.subspacecomputing.com/developer
|
|
10
10
|
Project-URL: Support, https://www.subspacecomputing.com/developer
|
|
@@ -20,6 +20,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
20
20
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
21
21
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
22
22
|
Requires-Dist: fastapi>=0.100.0; extra == "dev"
|
|
23
|
+
Requires-Dist: httpx>=0.25.0; extra == "dev"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: home-page
|
|
25
26
|
Dynamic: license-file
|
|
@@ -40,13 +41,24 @@ pip install subspacecomputing
|
|
|
40
41
|
### Initialization
|
|
41
42
|
|
|
42
43
|
```python
|
|
43
|
-
from subspacecomputing import
|
|
44
|
+
from subspacecomputing import ASTRIA
|
|
44
45
|
|
|
45
46
|
# Initialize the client (defaults to production URL)
|
|
46
|
-
|
|
47
|
+
client = ASTRIA(api_key='your-api-key-here')
|
|
47
48
|
|
|
48
49
|
# For local testing or custom environments (optional)
|
|
49
|
-
#
|
|
50
|
+
# client = ASTRIA(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Teams (`X-Team-Id`)
|
|
54
|
+
|
|
55
|
+
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`.
|
|
56
|
+
|
|
57
|
+
If you omit it when required, the API returns **400** with `error.reason` **`team_context_required`**. Use `ValidationError` and read `exc.reason`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = ASTRIA(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
61
|
+
client.set_team_id(None) # clear override for following requests
|
|
50
62
|
```
|
|
51
63
|
|
|
52
64
|
### Simple Projection (1 scenario)
|
|
@@ -66,7 +78,7 @@ spec = {
|
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
# Run the projection
|
|
69
|
-
result =
|
|
81
|
+
result = client.project(spec)
|
|
70
82
|
|
|
71
83
|
# Display results
|
|
72
84
|
print(f"Final capital: {result['final_values']['capital']}")
|
|
@@ -96,7 +108,7 @@ spec = {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
# Run the simulation
|
|
99
|
-
result =
|
|
111
|
+
result = client.simulate(spec)
|
|
100
112
|
|
|
101
113
|
# Analyze results
|
|
102
114
|
print(f"Mean final capital: {result['last_mean']['capital']}")
|
|
@@ -150,7 +162,7 @@ aggregations = [
|
|
|
150
162
|
]
|
|
151
163
|
|
|
152
164
|
# Run batch
|
|
153
|
-
result =
|
|
165
|
+
result = client.project_batch(
|
|
154
166
|
template=template,
|
|
155
167
|
batch_params=batch_params,
|
|
156
168
|
aggregations=aggregations
|
|
@@ -168,7 +180,7 @@ print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
|
168
180
|
|
|
169
181
|
```python
|
|
170
182
|
# Validate an SP Model before execution
|
|
171
|
-
validation =
|
|
183
|
+
validation = client.validate(spec)
|
|
172
184
|
|
|
173
185
|
if validation['is_valid']:
|
|
174
186
|
print("✅ SP Model is valid")
|
|
@@ -182,15 +194,15 @@ else:
|
|
|
182
194
|
|
|
183
195
|
```python
|
|
184
196
|
# Get examples
|
|
185
|
-
examples =
|
|
197
|
+
examples = client.get_examples()
|
|
186
198
|
print(f"Available examples: {len(examples['examples'])}")
|
|
187
199
|
|
|
188
200
|
# Check usage
|
|
189
|
-
usage =
|
|
201
|
+
usage = client.get_usage()
|
|
190
202
|
print(f"Simulations used: {usage['usage']['simulations_used']}/{usage['usage']['simulations_limit']}")
|
|
191
203
|
|
|
192
204
|
# Get plans
|
|
193
|
-
plans =
|
|
205
|
+
plans = client.get_plans()
|
|
194
206
|
for plan in plans['plans']:
|
|
195
207
|
print(f"{plan['name']}: ${plan['price_monthly']}/month")
|
|
196
208
|
```
|
|
@@ -199,16 +211,16 @@ for plan in plans['plans']:
|
|
|
199
211
|
|
|
200
212
|
```python
|
|
201
213
|
from subspacecomputing import (
|
|
202
|
-
|
|
214
|
+
Subspace,
|
|
215
|
+
SubspaceError,
|
|
203
216
|
QuotaExceededError,
|
|
204
217
|
RateLimitError,
|
|
205
218
|
AuthenticationError,
|
|
206
219
|
ValidationError,
|
|
207
|
-
BSCEError
|
|
208
220
|
)
|
|
209
221
|
|
|
210
222
|
try:
|
|
211
|
-
result =
|
|
223
|
+
result = client.simulate(spec)
|
|
212
224
|
except QuotaExceededError as e:
|
|
213
225
|
print(f"Monthly quota exceeded: {e}")
|
|
214
226
|
except RateLimitError as e:
|
|
@@ -218,7 +230,7 @@ except AuthenticationError as e:
|
|
|
218
230
|
print(f"Invalid API key: {e}")
|
|
219
231
|
except ValidationError as e:
|
|
220
232
|
print(f"Validation error: {e.detail}")
|
|
221
|
-
except
|
|
233
|
+
except SubspaceError as e:
|
|
222
234
|
print(f"API error: {e}")
|
|
223
235
|
```
|
|
224
236
|
|
|
@@ -228,15 +240,15 @@ After making a request, you can check your rate limit and quota status:
|
|
|
228
240
|
|
|
229
241
|
```python
|
|
230
242
|
# Make a request
|
|
231
|
-
result =
|
|
243
|
+
result = client.project(spec)
|
|
232
244
|
|
|
233
245
|
# Check rate limit info
|
|
234
|
-
rate_limit =
|
|
246
|
+
rate_limit = client.get_rate_limit_info()
|
|
235
247
|
if rate_limit:
|
|
236
248
|
print(f"Rate limit: {rate_limit['remaining']}/{rate_limit['limit']} remaining")
|
|
237
249
|
|
|
238
250
|
# Check quota info
|
|
239
|
-
quota =
|
|
251
|
+
quota = client.get_quota_info()
|
|
240
252
|
if quota:
|
|
241
253
|
print(f"Quota: {quota['used']}/{quota['limit']} used, {quota['remaining']} remaining")
|
|
242
254
|
```
|
|
@@ -13,13 +13,24 @@ pip install subspacecomputing
|
|
|
13
13
|
### Initialization
|
|
14
14
|
|
|
15
15
|
```python
|
|
16
|
-
from subspacecomputing import
|
|
16
|
+
from subspacecomputing import ASTRIA
|
|
17
17
|
|
|
18
18
|
# Initialize the client (defaults to production URL)
|
|
19
|
-
|
|
19
|
+
client = ASTRIA(api_key='your-api-key-here')
|
|
20
20
|
|
|
21
21
|
# For local testing or custom environments (optional)
|
|
22
|
-
#
|
|
22
|
+
# client = ASTRIA(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
23
|
+
```
|
|
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
|
+
client = ASTRIA(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
33
|
+
client.set_team_id(None) # clear override for following requests
|
|
23
34
|
```
|
|
24
35
|
|
|
25
36
|
### Simple Projection (1 scenario)
|
|
@@ -39,7 +50,7 @@ spec = {
|
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
# Run the projection
|
|
42
|
-
result =
|
|
53
|
+
result = client.project(spec)
|
|
43
54
|
|
|
44
55
|
# Display results
|
|
45
56
|
print(f"Final capital: {result['final_values']['capital']}")
|
|
@@ -69,7 +80,7 @@ spec = {
|
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
# Run the simulation
|
|
72
|
-
result =
|
|
83
|
+
result = client.simulate(spec)
|
|
73
84
|
|
|
74
85
|
# Analyze results
|
|
75
86
|
print(f"Mean final capital: {result['last_mean']['capital']}")
|
|
@@ -123,7 +134,7 @@ aggregations = [
|
|
|
123
134
|
]
|
|
124
135
|
|
|
125
136
|
# Run batch
|
|
126
|
-
result =
|
|
137
|
+
result = client.project_batch(
|
|
127
138
|
template=template,
|
|
128
139
|
batch_params=batch_params,
|
|
129
140
|
aggregations=aggregations
|
|
@@ -141,7 +152,7 @@ print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
|
141
152
|
|
|
142
153
|
```python
|
|
143
154
|
# Validate an SP Model before execution
|
|
144
|
-
validation =
|
|
155
|
+
validation = client.validate(spec)
|
|
145
156
|
|
|
146
157
|
if validation['is_valid']:
|
|
147
158
|
print("✅ SP Model is valid")
|
|
@@ -155,15 +166,15 @@ else:
|
|
|
155
166
|
|
|
156
167
|
```python
|
|
157
168
|
# Get examples
|
|
158
|
-
examples =
|
|
169
|
+
examples = client.get_examples()
|
|
159
170
|
print(f"Available examples: {len(examples['examples'])}")
|
|
160
171
|
|
|
161
172
|
# Check usage
|
|
162
|
-
usage =
|
|
173
|
+
usage = client.get_usage()
|
|
163
174
|
print(f"Simulations used: {usage['usage']['simulations_used']}/{usage['usage']['simulations_limit']}")
|
|
164
175
|
|
|
165
176
|
# Get plans
|
|
166
|
-
plans =
|
|
177
|
+
plans = client.get_plans()
|
|
167
178
|
for plan in plans['plans']:
|
|
168
179
|
print(f"{plan['name']}: ${plan['price_monthly']}/month")
|
|
169
180
|
```
|
|
@@ -172,16 +183,16 @@ for plan in plans['plans']:
|
|
|
172
183
|
|
|
173
184
|
```python
|
|
174
185
|
from subspacecomputing import (
|
|
175
|
-
|
|
186
|
+
Subspace,
|
|
187
|
+
SubspaceError,
|
|
176
188
|
QuotaExceededError,
|
|
177
189
|
RateLimitError,
|
|
178
190
|
AuthenticationError,
|
|
179
191
|
ValidationError,
|
|
180
|
-
BSCEError
|
|
181
192
|
)
|
|
182
193
|
|
|
183
194
|
try:
|
|
184
|
-
result =
|
|
195
|
+
result = client.simulate(spec)
|
|
185
196
|
except QuotaExceededError as e:
|
|
186
197
|
print(f"Monthly quota exceeded: {e}")
|
|
187
198
|
except RateLimitError as e:
|
|
@@ -191,7 +202,7 @@ except AuthenticationError as e:
|
|
|
191
202
|
print(f"Invalid API key: {e}")
|
|
192
203
|
except ValidationError as e:
|
|
193
204
|
print(f"Validation error: {e.detail}")
|
|
194
|
-
except
|
|
205
|
+
except SubspaceError as e:
|
|
195
206
|
print(f"API error: {e}")
|
|
196
207
|
```
|
|
197
208
|
|
|
@@ -201,15 +212,15 @@ After making a request, you can check your rate limit and quota status:
|
|
|
201
212
|
|
|
202
213
|
```python
|
|
203
214
|
# Make a request
|
|
204
|
-
result =
|
|
215
|
+
result = client.project(spec)
|
|
205
216
|
|
|
206
217
|
# Check rate limit info
|
|
207
|
-
rate_limit =
|
|
218
|
+
rate_limit = client.get_rate_limit_info()
|
|
208
219
|
if rate_limit:
|
|
209
220
|
print(f"Rate limit: {rate_limit['remaining']}/{rate_limit['limit']} remaining")
|
|
210
221
|
|
|
211
222
|
# Check quota info
|
|
212
|
-
quota =
|
|
223
|
+
quota = client.get_quota_info()
|
|
213
224
|
if quota:
|
|
214
225
|
print(f"Quota: {quota['used']}/{quota['limit']} used, {quota['remaining']} remaining")
|
|
215
226
|
```
|
|
@@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "subspacecomputing"
|
|
9
|
-
version = "0.1.
|
|
10
|
-
description = "Python SDK for Subspace Computing Engine
|
|
11
|
-
authors = [{name = "
|
|
9
|
+
version = "0.1.4"
|
|
10
|
+
description = "Python SDK for ASTRIA — Subspace Computing Engine"
|
|
11
|
+
authors = [{name = "Subspace Computing", email = "contact@beausoft.ca"}]
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
dependencies = [
|
|
@@ -22,9 +22,11 @@ dev = [
|
|
|
22
22
|
"pytest-cov>=4.0.0",
|
|
23
23
|
"black>=23.0.0",
|
|
24
24
|
"ruff>=0.1.0",
|
|
25
|
-
"fastapi>=0.100.0",
|
|
25
|
+
"fastapi>=0.100.0",
|
|
26
|
+
"httpx>=0.25.0", # TestClient (tests d'intégration)
|
|
26
27
|
]
|
|
27
28
|
|
|
29
|
+
# No Git repository URL: built from a private monorepo; only public product URLs are published on PyPI.
|
|
28
30
|
[project.urls]
|
|
29
31
|
Documentation = "https://www.subspacecomputing.com/developer"
|
|
30
32
|
Homepage = "https://www.subspacecomputing.com/developer"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Setup.py pour le SDK Python
|
|
2
|
+
Setup.py pour le SDK Python ASTRIA — Subspace Computing Engine.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from setuptools import setup, find_packages
|
|
@@ -13,11 +13,11 @@ except FileNotFoundError:
|
|
|
13
13
|
|
|
14
14
|
setup(
|
|
15
15
|
name="subspacecomputing",
|
|
16
|
-
version="0.1.
|
|
17
|
-
description="Python SDK for Subspace Computing Engine
|
|
16
|
+
version="0.1.4",
|
|
17
|
+
description="Python SDK for ASTRIA — Subspace Computing Engine",
|
|
18
18
|
long_description=long_description,
|
|
19
19
|
long_description_content_type="text/markdown",
|
|
20
|
-
author="
|
|
20
|
+
author="Subspace Computing",
|
|
21
21
|
author_email="contact@beausoft.ca",
|
|
22
22
|
url="https://www.subspacecomputing.com/developer",
|
|
23
23
|
packages=find_packages(),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ASTRIA — Python SDK
|
|
3
|
+
|
|
4
|
+
Python SDK for ASTRIA, the Subspace Computing Engine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import ASTRIA, Subspace, BSCE # Subspace & BSCE = backward compat aliases
|
|
8
|
+
from .errors import (
|
|
9
|
+
SubspaceError,
|
|
10
|
+
BSCEError, # backward compat alias
|
|
11
|
+
QuotaExceededError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.4"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ASTRIA",
|
|
20
|
+
"SubspaceError",
|
|
21
|
+
"QuotaExceededError",
|
|
22
|
+
"RateLimitError",
|
|
23
|
+
"AuthenticationError",
|
|
24
|
+
"ValidationError",
|
|
25
|
+
# Backward compatibility
|
|
26
|
+
"Subspace",
|
|
27
|
+
"BSCE",
|
|
28
|
+
"BSCEError",
|
|
29
|
+
]
|
|
@@ -7,7 +7,7 @@ This module provides a simple Python interface to interact with the Subspace Com
|
|
|
7
7
|
import requests
|
|
8
8
|
from typing import Dict, Any, Optional, List
|
|
9
9
|
from .errors import (
|
|
10
|
-
|
|
10
|
+
SubspaceError,
|
|
11
11
|
QuotaExceededError,
|
|
12
12
|
RateLimitError,
|
|
13
13
|
AuthenticationError,
|
|
@@ -18,17 +18,19 @@ from .errors import (
|
|
|
18
18
|
DEFAULT_TIMEOUT = 60
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class
|
|
22
|
-
"""
|
|
21
|
+
class ASTRIA:
|
|
22
|
+
"""ASTRIA — Moteur de calcul Subspace Computing. Client Python pour l'API."""
|
|
23
23
|
|
|
24
24
|
def __init__(
|
|
25
25
|
self,
|
|
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
|
-
Initialize the
|
|
33
|
+
Initialize the ASTRIA client.
|
|
32
34
|
|
|
33
35
|
Args:
|
|
34
36
|
api_key: Your API key (format: be_live_... or sk_pro_...).
|
|
@@ -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.
|
|
@@ -66,7 +81,7 @@ class BSCE:
|
|
|
66
81
|
QuotaExceededError: If monthly quota exceeded (429)
|
|
67
82
|
AuthenticationError: If API key invalid (401)
|
|
68
83
|
ValidationError: If validation error (400)
|
|
69
|
-
|
|
84
|
+
SubspaceError: For other errors
|
|
70
85
|
"""
|
|
71
86
|
if response.status_code == 429:
|
|
72
87
|
# Check if it's rate limit or quota
|
|
@@ -81,11 +96,11 @@ class BSCE:
|
|
|
81
96
|
response,
|
|
82
97
|
)
|
|
83
98
|
elif response.status_code == 403:
|
|
84
|
-
raise
|
|
99
|
+
raise SubspaceError("Access denied", response)
|
|
85
100
|
elif response.status_code == 400:
|
|
86
101
|
raise ValidationError("Validation error", response)
|
|
87
102
|
elif not response.ok:
|
|
88
|
-
raise
|
|
103
|
+
raise SubspaceError(f"API error: {response.status_code}", response)
|
|
89
104
|
|
|
90
105
|
# Main endpoints (calculations)
|
|
91
106
|
|
|
@@ -331,7 +346,7 @@ class BSCE:
|
|
|
331
346
|
projection_id: Projection ID
|
|
332
347
|
|
|
333
348
|
Raises:
|
|
334
|
-
|
|
349
|
+
SubspaceError: If error during deletion
|
|
335
350
|
"""
|
|
336
351
|
response = self.session.delete(
|
|
337
352
|
f"{self.base_url}/projections/{projection_id}",
|
|
@@ -435,7 +450,7 @@ class BSCE:
|
|
|
435
450
|
run_id: Run ID
|
|
436
451
|
|
|
437
452
|
Raises:
|
|
438
|
-
|
|
453
|
+
SubspaceError: If error during deletion
|
|
439
454
|
"""
|
|
440
455
|
response = self.session.delete(
|
|
441
456
|
f"{self.base_url}/projection-runs/{run_id}",
|
|
@@ -445,6 +460,24 @@ class BSCE:
|
|
|
445
460
|
self._handle_response(response)
|
|
446
461
|
# 204 No Content, no body
|
|
447
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
|
+
|
|
448
481
|
# Helper methods for accessing rate limit and quota info
|
|
449
482
|
|
|
450
483
|
@staticmethod
|
|
@@ -497,3 +530,8 @@ class BSCE:
|
|
|
497
530
|
"remaining": self._safe_int(remaining),
|
|
498
531
|
"used": self._safe_int(used),
|
|
499
532
|
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# Backward compatibility aliases
|
|
536
|
+
Subspace = ASTRIA
|
|
537
|
+
BSCE = ASTRIA
|
|
@@ -3,54 +3,70 @@ 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
|
-
class
|
|
9
|
+
class SubspaceError(Exception):
|
|
10
10
|
"""Base error for the Subspace Computing SDK."""
|
|
11
11
|
|
|
12
12
|
def __init__(self, message: str, response: Optional[requests.Response] = None):
|
|
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
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
|
+
"""Structured error reason string, if the response body contains ``{"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}"
|
|
32
44
|
return self.message
|
|
33
45
|
|
|
34
46
|
|
|
35
|
-
class QuotaExceededError(
|
|
47
|
+
class QuotaExceededError(SubspaceError):
|
|
36
48
|
"""Monthly quota exceeded (429)."""
|
|
37
49
|
|
|
38
50
|
pass
|
|
39
51
|
|
|
40
52
|
|
|
41
|
-
class RateLimitError(
|
|
53
|
+
class RateLimitError(SubspaceError):
|
|
42
54
|
"""Rate limit exceeded (429)."""
|
|
43
55
|
|
|
44
56
|
pass
|
|
45
57
|
|
|
46
58
|
|
|
47
|
-
class AuthenticationError(
|
|
59
|
+
class AuthenticationError(SubspaceError):
|
|
48
60
|
"""Invalid or missing API key (401)."""
|
|
49
61
|
|
|
50
62
|
pass
|
|
51
63
|
|
|
52
64
|
|
|
53
|
-
class ValidationError(
|
|
65
|
+
class ValidationError(SubspaceError):
|
|
54
66
|
"""SP Model validation error (400)."""
|
|
55
67
|
|
|
56
68
|
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Backward compatibility alias
|
|
72
|
+
BSCEError = SubspaceError
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: subspacecomputing
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Python SDK for Subspace Computing Engine
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Python SDK for ASTRIA — Subspace Computing Engine
|
|
5
5
|
Home-page: https://www.subspacecomputing.com/developer
|
|
6
|
-
Author:
|
|
7
|
-
Author-email:
|
|
6
|
+
Author: Subspace Computing
|
|
7
|
+
Author-email: Subspace Computing <contact@beausoft.ca>
|
|
8
8
|
Project-URL: Documentation, https://www.subspacecomputing.com/developer
|
|
9
9
|
Project-URL: Homepage, https://www.subspacecomputing.com/developer
|
|
10
10
|
Project-URL: Support, https://www.subspacecomputing.com/developer
|
|
@@ -20,6 +20,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
20
20
|
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
21
21
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
22
22
|
Requires-Dist: fastapi>=0.100.0; extra == "dev"
|
|
23
|
+
Requires-Dist: httpx>=0.25.0; extra == "dev"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: home-page
|
|
25
26
|
Dynamic: license-file
|
|
@@ -40,13 +41,24 @@ pip install subspacecomputing
|
|
|
40
41
|
### Initialization
|
|
41
42
|
|
|
42
43
|
```python
|
|
43
|
-
from subspacecomputing import
|
|
44
|
+
from subspacecomputing import ASTRIA
|
|
44
45
|
|
|
45
46
|
# Initialize the client (defaults to production URL)
|
|
46
|
-
|
|
47
|
+
client = ASTRIA(api_key='your-api-key-here')
|
|
47
48
|
|
|
48
49
|
# For local testing or custom environments (optional)
|
|
49
|
-
#
|
|
50
|
+
# client = ASTRIA(api_key='your-api-key-here', base_url='http://localhost:8000')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Teams (`X-Team-Id`)
|
|
54
|
+
|
|
55
|
+
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`.
|
|
56
|
+
|
|
57
|
+
If you omit it when required, the API returns **400** with `error.reason` **`team_context_required`**. Use `ValidationError` and read `exc.reason`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = ASTRIA(api_key="...", team_id="00000000-0000-0000-0000-000000000000")
|
|
61
|
+
client.set_team_id(None) # clear override for following requests
|
|
50
62
|
```
|
|
51
63
|
|
|
52
64
|
### Simple Projection (1 scenario)
|
|
@@ -66,7 +78,7 @@ spec = {
|
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
# Run the projection
|
|
69
|
-
result =
|
|
81
|
+
result = client.project(spec)
|
|
70
82
|
|
|
71
83
|
# Display results
|
|
72
84
|
print(f"Final capital: {result['final_values']['capital']}")
|
|
@@ -96,7 +108,7 @@ spec = {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
# Run the simulation
|
|
99
|
-
result =
|
|
111
|
+
result = client.simulate(spec)
|
|
100
112
|
|
|
101
113
|
# Analyze results
|
|
102
114
|
print(f"Mean final capital: {result['last_mean']['capital']}")
|
|
@@ -150,7 +162,7 @@ aggregations = [
|
|
|
150
162
|
]
|
|
151
163
|
|
|
152
164
|
# Run batch
|
|
153
|
-
result =
|
|
165
|
+
result = client.project_batch(
|
|
154
166
|
template=template,
|
|
155
167
|
batch_params=batch_params,
|
|
156
168
|
aggregations=aggregations
|
|
@@ -168,7 +180,7 @@ print(f"Average: {result['aggregations']['moyenne_capital']}")
|
|
|
168
180
|
|
|
169
181
|
```python
|
|
170
182
|
# Validate an SP Model before execution
|
|
171
|
-
validation =
|
|
183
|
+
validation = client.validate(spec)
|
|
172
184
|
|
|
173
185
|
if validation['is_valid']:
|
|
174
186
|
print("✅ SP Model is valid")
|
|
@@ -182,15 +194,15 @@ else:
|
|
|
182
194
|
|
|
183
195
|
```python
|
|
184
196
|
# Get examples
|
|
185
|
-
examples =
|
|
197
|
+
examples = client.get_examples()
|
|
186
198
|
print(f"Available examples: {len(examples['examples'])}")
|
|
187
199
|
|
|
188
200
|
# Check usage
|
|
189
|
-
usage =
|
|
201
|
+
usage = client.get_usage()
|
|
190
202
|
print(f"Simulations used: {usage['usage']['simulations_used']}/{usage['usage']['simulations_limit']}")
|
|
191
203
|
|
|
192
204
|
# Get plans
|
|
193
|
-
plans =
|
|
205
|
+
plans = client.get_plans()
|
|
194
206
|
for plan in plans['plans']:
|
|
195
207
|
print(f"{plan['name']}: ${plan['price_monthly']}/month")
|
|
196
208
|
```
|
|
@@ -199,16 +211,16 @@ for plan in plans['plans']:
|
|
|
199
211
|
|
|
200
212
|
```python
|
|
201
213
|
from subspacecomputing import (
|
|
202
|
-
|
|
214
|
+
Subspace,
|
|
215
|
+
SubspaceError,
|
|
203
216
|
QuotaExceededError,
|
|
204
217
|
RateLimitError,
|
|
205
218
|
AuthenticationError,
|
|
206
219
|
ValidationError,
|
|
207
|
-
BSCEError
|
|
208
220
|
)
|
|
209
221
|
|
|
210
222
|
try:
|
|
211
|
-
result =
|
|
223
|
+
result = client.simulate(spec)
|
|
212
224
|
except QuotaExceededError as e:
|
|
213
225
|
print(f"Monthly quota exceeded: {e}")
|
|
214
226
|
except RateLimitError as e:
|
|
@@ -218,7 +230,7 @@ except AuthenticationError as e:
|
|
|
218
230
|
print(f"Invalid API key: {e}")
|
|
219
231
|
except ValidationError as e:
|
|
220
232
|
print(f"Validation error: {e.detail}")
|
|
221
|
-
except
|
|
233
|
+
except SubspaceError as e:
|
|
222
234
|
print(f"API error: {e}")
|
|
223
235
|
```
|
|
224
236
|
|
|
@@ -228,15 +240,15 @@ After making a request, you can check your rate limit and quota status:
|
|
|
228
240
|
|
|
229
241
|
```python
|
|
230
242
|
# Make a request
|
|
231
|
-
result =
|
|
243
|
+
result = client.project(spec)
|
|
232
244
|
|
|
233
245
|
# Check rate limit info
|
|
234
|
-
rate_limit =
|
|
246
|
+
rate_limit = client.get_rate_limit_info()
|
|
235
247
|
if rate_limit:
|
|
236
248
|
print(f"Rate limit: {rate_limit['remaining']}/{rate_limit['limit']} remaining")
|
|
237
249
|
|
|
238
250
|
# Check quota info
|
|
239
|
-
quota =
|
|
251
|
+
quota = client.get_quota_info()
|
|
240
252
|
if quota:
|
|
241
253
|
print(f"Quota: {quota['used']}/{quota['limit']} used, {quota['remaining']} remaining")
|
|
242
254
|
```
|
|
@@ -15,7 +15,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
|
15
15
|
|
|
16
16
|
from fastapi.testclient import TestClient
|
|
17
17
|
from api.main import app
|
|
18
|
-
from subspacecomputing import
|
|
18
|
+
from subspacecomputing import ASTRIA
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@pytest.fixture
|
|
@@ -33,7 +33,7 @@ def sdk_client(api_key, api_client):
|
|
|
33
33
|
fonctionne avec le SDK qui utilise requests.Session.
|
|
34
34
|
"""
|
|
35
35
|
# Créer une instance du SDK
|
|
36
|
-
client =
|
|
36
|
+
client = ASTRIA(api_key=api_key, base_url="http://testserver")
|
|
37
37
|
|
|
38
38
|
# Créer un mock pour requests.Session qui utilise TestClient
|
|
39
39
|
def make_request(method, url, **kwargs):
|
|
@@ -7,8 +7,8 @@ Ces tests vérifient le comportement du SDK sans dépendre de l'API réelle.
|
|
|
7
7
|
import pytest
|
|
8
8
|
from unittest.mock import Mock, patch
|
|
9
9
|
from subspacecomputing import (
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
ASTRIA,
|
|
11
|
+
SubspaceError,
|
|
12
12
|
QuotaExceededError,
|
|
13
13
|
RateLimitError,
|
|
14
14
|
AuthenticationError,
|
|
@@ -21,30 +21,30 @@ class TestClientInitialization:
|
|
|
21
21
|
|
|
22
22
|
def test_init_with_api_key(self, api_key, base_url):
|
|
23
23
|
"""Test initialisation avec API key."""
|
|
24
|
-
|
|
25
|
-
assert
|
|
26
|
-
assert
|
|
27
|
-
assert 'X-API-Key' in
|
|
28
|
-
assert
|
|
24
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
25
|
+
assert client.api_key == api_key
|
|
26
|
+
assert client.base_url == base_url
|
|
27
|
+
assert 'X-API-Key' in client.session.headers
|
|
28
|
+
assert client.session.headers['X-API-Key'] == api_key
|
|
29
29
|
|
|
30
30
|
def test_init_default_base_url(self, api_key):
|
|
31
31
|
"""Test initialisation avec URL par défaut."""
|
|
32
|
-
|
|
33
|
-
assert
|
|
32
|
+
client = ASTRIA(api_key=api_key)
|
|
33
|
+
assert client.base_url == 'https://api.subspacecomputing.com'
|
|
34
34
|
|
|
35
35
|
def test_init_strips_trailing_slash(self, api_key):
|
|
36
36
|
"""Test que l'URL est nettoyée (pas de slash final)."""
|
|
37
|
-
|
|
38
|
-
assert
|
|
37
|
+
client = ASTRIA(api_key=api_key, base_url='http://localhost:8000/')
|
|
38
|
+
assert client.base_url == 'http://localhost:8000'
|
|
39
39
|
|
|
40
40
|
def test_init_rejects_empty_api_key(self):
|
|
41
41
|
"""Test que api_key vide lève ValueError."""
|
|
42
42
|
with pytest.raises(ValueError, match="API key is required"):
|
|
43
|
-
|
|
43
|
+
ASTRIA(api_key="")
|
|
44
44
|
with pytest.raises(ValueError, match="API key is required"):
|
|
45
|
-
|
|
45
|
+
ASTRIA(api_key=" ")
|
|
46
46
|
with pytest.raises(ValueError, match="API key is required"):
|
|
47
|
-
|
|
47
|
+
ASTRIA(api_key=None)
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class TestErrorHandling:
|
|
@@ -52,7 +52,7 @@ class TestErrorHandling:
|
|
|
52
52
|
|
|
53
53
|
def test_rate_limit_error(self, api_key, base_url):
|
|
54
54
|
"""Test gestion erreur 429 (rate limit)."""
|
|
55
|
-
|
|
55
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
56
56
|
|
|
57
57
|
mock_response = Mock()
|
|
58
58
|
mock_response.status_code = 429
|
|
@@ -60,14 +60,14 @@ class TestErrorHandling:
|
|
|
60
60
|
mock_response.headers = {"Retry-After": "60"}
|
|
61
61
|
mock_response.ok = False
|
|
62
62
|
|
|
63
|
-
with patch.object(
|
|
63
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
64
64
|
with pytest.raises(RateLimitError) as exc_info:
|
|
65
|
-
|
|
65
|
+
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
66
66
|
assert "Rate limit" in str(exc_info.value)
|
|
67
67
|
|
|
68
68
|
def test_quota_exceeded_error(self, api_key, base_url):
|
|
69
69
|
"""Test gestion erreur 429 (quota mensuel)."""
|
|
70
|
-
|
|
70
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
71
71
|
|
|
72
72
|
mock_response = Mock()
|
|
73
73
|
mock_response.status_code = 429
|
|
@@ -75,28 +75,28 @@ class TestErrorHandling:
|
|
|
75
75
|
mock_response.headers = {}
|
|
76
76
|
mock_response.ok = False
|
|
77
77
|
|
|
78
|
-
with patch.object(
|
|
78
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
79
79
|
with pytest.raises(QuotaExceededError) as exc_info:
|
|
80
|
-
|
|
80
|
+
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
81
81
|
assert "quota" in str(exc_info.value).lower()
|
|
82
82
|
|
|
83
83
|
def test_authentication_error(self, api_key, base_url):
|
|
84
84
|
"""Test gestion erreur 401 (API key invalide)."""
|
|
85
|
-
|
|
85
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
86
86
|
|
|
87
87
|
mock_response = Mock()
|
|
88
88
|
mock_response.status_code = 401
|
|
89
89
|
mock_response.text = "Invalid API key"
|
|
90
90
|
mock_response.ok = False
|
|
91
91
|
|
|
92
|
-
with patch.object(
|
|
92
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
93
93
|
with pytest.raises(AuthenticationError) as exc_info:
|
|
94
|
-
|
|
94
|
+
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
95
95
|
assert "API key" in str(exc_info.value)
|
|
96
96
|
|
|
97
97
|
def test_validation_error(self, api_key, base_url):
|
|
98
98
|
"""Test gestion erreur 400 (validation)."""
|
|
99
|
-
|
|
99
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
100
100
|
|
|
101
101
|
mock_response = Mock()
|
|
102
102
|
mock_response.status_code = 400
|
|
@@ -104,23 +104,23 @@ class TestErrorHandling:
|
|
|
104
104
|
mock_response.json.return_value = {'detail': 'Invalid formula'}
|
|
105
105
|
mock_response.ok = False
|
|
106
106
|
|
|
107
|
-
with patch.object(
|
|
107
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
108
108
|
with pytest.raises(ValidationError) as exc_info:
|
|
109
|
-
|
|
109
|
+
client.validate({'scenarios': 1, 'steps': 12, 'variables': []})
|
|
110
110
|
assert "validation" in str(exc_info.value).lower()
|
|
111
111
|
|
|
112
112
|
def test_generic_error(self, api_key, base_url):
|
|
113
113
|
"""Test gestion erreur générique (500)."""
|
|
114
|
-
|
|
114
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
115
115
|
|
|
116
116
|
mock_response = Mock()
|
|
117
117
|
mock_response.status_code = 500
|
|
118
118
|
mock_response.text = "Internal server error"
|
|
119
119
|
mock_response.ok = False
|
|
120
120
|
|
|
121
|
-
with patch.object(
|
|
122
|
-
with pytest.raises(
|
|
123
|
-
|
|
121
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
122
|
+
with pytest.raises(SubspaceError) as exc_info:
|
|
123
|
+
client.simulate({'scenarios': 100, 'steps': 12, 'variables': []})
|
|
124
124
|
assert "500" in str(exc_info.value)
|
|
125
125
|
|
|
126
126
|
|
|
@@ -129,7 +129,7 @@ class TestEndpoints:
|
|
|
129
129
|
|
|
130
130
|
def test_project_success(self, api_key, base_url, simple_spec):
|
|
131
131
|
"""Test endpoint project() avec succès."""
|
|
132
|
-
|
|
132
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
133
133
|
|
|
134
134
|
mock_response = Mock()
|
|
135
135
|
mock_response.status_code = 200
|
|
@@ -142,15 +142,15 @@ class TestEndpoints:
|
|
|
142
142
|
'performance': {'execution_time_ms': 0.5}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
with patch.object(
|
|
146
|
-
result =
|
|
145
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
146
|
+
result = client.project(simple_spec)
|
|
147
147
|
assert result['scenarios'] == 1
|
|
148
148
|
assert 'final_values' in result
|
|
149
149
|
assert 'trajectory' in result
|
|
150
150
|
|
|
151
151
|
def test_simulate_success(self, api_key, base_url, simulation_spec):
|
|
152
152
|
"""Test endpoint simulate() avec succès."""
|
|
153
|
-
|
|
153
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
154
154
|
|
|
155
155
|
mock_response = Mock()
|
|
156
156
|
mock_response.status_code = 200
|
|
@@ -169,15 +169,15 @@ class TestEndpoints:
|
|
|
169
169
|
'performance': {'execution_time_ms': 12.5}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
with patch.object(
|
|
173
|
-
result =
|
|
172
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
173
|
+
result = client.simulate(simulation_spec)
|
|
174
174
|
assert result['scenarios'] == 100
|
|
175
175
|
assert 'last_mean' in result
|
|
176
176
|
assert 'statistics' in result
|
|
177
177
|
|
|
178
178
|
def test_project_batch_success(self, api_key, base_url, batch_spec_simple):
|
|
179
179
|
"""Test endpoint project_batch() avec succès."""
|
|
180
|
-
|
|
180
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
181
181
|
|
|
182
182
|
mock_response = Mock()
|
|
183
183
|
mock_response.status_code = 200
|
|
@@ -197,8 +197,8 @@ class TestEndpoints:
|
|
|
197
197
|
'performance': {'execution_time_ms': 25.0}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
with patch.object(
|
|
201
|
-
result =
|
|
200
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
201
|
+
result = client.project_batch(
|
|
202
202
|
template=batch_spec_simple['template'],
|
|
203
203
|
batch_params=batch_spec_simple['batch_params']
|
|
204
204
|
)
|
|
@@ -207,7 +207,7 @@ class TestEndpoints:
|
|
|
207
207
|
|
|
208
208
|
def test_validate_success(self, api_key, base_url, simple_spec):
|
|
209
209
|
"""Test endpoint validate() avec succès."""
|
|
210
|
-
|
|
210
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
211
211
|
|
|
212
212
|
mock_response = Mock()
|
|
213
213
|
mock_response.status_code = 200
|
|
@@ -218,14 +218,14 @@ class TestEndpoints:
|
|
|
218
218
|
'warnings': []
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
with patch.object(
|
|
222
|
-
result =
|
|
221
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
222
|
+
result = client.validate(simple_spec)
|
|
223
223
|
assert result['valid'] is True
|
|
224
224
|
assert len(result['errors']) == 0
|
|
225
225
|
|
|
226
226
|
def test_get_examples_success(self, api_key, base_url):
|
|
227
227
|
"""Test endpoint get_examples() avec succès."""
|
|
228
|
-
|
|
228
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
229
229
|
|
|
230
230
|
mock_response = Mock()
|
|
231
231
|
mock_response.status_code = 200
|
|
@@ -237,13 +237,13 @@ class TestEndpoints:
|
|
|
237
237
|
]
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
result =
|
|
240
|
+
client.session.get = Mock(return_value=mock_response)
|
|
241
|
+
result = client.get_examples()
|
|
242
242
|
assert len(result['examples']) == 2
|
|
243
243
|
|
|
244
244
|
def test_get_usage_success(self, api_key, base_url):
|
|
245
245
|
"""Test endpoint get_usage() avec succès."""
|
|
246
|
-
|
|
246
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
247
247
|
|
|
248
248
|
mock_response = Mock()
|
|
249
249
|
mock_response.status_code = 200
|
|
@@ -259,14 +259,14 @@ class TestEndpoints:
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
with patch.object(
|
|
263
|
-
result =
|
|
262
|
+
with patch.object(client.session, 'get', return_value=mock_response):
|
|
263
|
+
result = client.get_usage()
|
|
264
264
|
assert 'usage' in result
|
|
265
265
|
assert result['usage']['simulations_used'] == 45
|
|
266
266
|
|
|
267
267
|
def test_replay_run_success(self, api_key, base_url):
|
|
268
268
|
"""Test endpoint replay_run() avec succès."""
|
|
269
|
-
|
|
269
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
270
270
|
|
|
271
271
|
mock_response = Mock()
|
|
272
272
|
mock_response.status_code = 200
|
|
@@ -281,8 +281,8 @@ class TestEndpoints:
|
|
|
281
281
|
'run_id': 'run-123'
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
with patch.object(
|
|
285
|
-
result =
|
|
284
|
+
with patch.object(client.session, 'post', return_value=mock_response):
|
|
285
|
+
result = client.replay_run(run_id='run-123', scenario_id=2)
|
|
286
286
|
assert result['scenarios'] == 1
|
|
287
287
|
assert result['steps'] == 6
|
|
288
288
|
assert 'final_values' in result
|
|
@@ -294,7 +294,7 @@ class TestBatchParameters:
|
|
|
294
294
|
|
|
295
295
|
def test_project_batch_with_aggregations(self, api_key, base_url):
|
|
296
296
|
"""Test project_batch() avec agrégations."""
|
|
297
|
-
|
|
297
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
298
298
|
|
|
299
299
|
template = {
|
|
300
300
|
'scenarios': 1,
|
|
@@ -319,8 +319,8 @@ class TestBatchParameters:
|
|
|
319
319
|
'aggregations': {'capital_total': 3000.0}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
with patch.object(
|
|
323
|
-
result =
|
|
322
|
+
with patch.object(client.session, 'post', return_value=mock_response) as mock_post:
|
|
323
|
+
result = client.project_batch(
|
|
324
324
|
template=template,
|
|
325
325
|
batch_params=batch_params,
|
|
326
326
|
aggregations=aggregations
|
|
@@ -337,43 +337,43 @@ class TestRateLimitQuotaInfo:
|
|
|
337
337
|
|
|
338
338
|
def test_get_rate_limit_info_unlimited(self, api_key, base_url):
|
|
339
339
|
"""get_rate_limit_info handles 'unlimited' without crashing."""
|
|
340
|
-
|
|
340
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
341
341
|
mock_response = Mock()
|
|
342
342
|
mock_response.headers = {"X-RateLimit-Limit": "unlimited", "X-RateLimit-Remaining": "999"}
|
|
343
|
-
|
|
343
|
+
client.last_response = mock_response
|
|
344
344
|
|
|
345
|
-
result =
|
|
345
|
+
result = client.get_rate_limit_info()
|
|
346
346
|
assert result["limit"] is None
|
|
347
347
|
assert result["remaining"] == 999
|
|
348
348
|
|
|
349
349
|
def test_get_quota_info_unlimited(self, api_key, base_url):
|
|
350
350
|
"""get_quota_info handles 'unlimited' in X-Quota-Limit."""
|
|
351
|
-
|
|
351
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
352
352
|
mock_response = Mock()
|
|
353
353
|
mock_response.headers = {
|
|
354
354
|
"X-Quota-Limit": "unlimited",
|
|
355
355
|
"X-Quota-Remaining": "100",
|
|
356
356
|
"X-Quota-Used": "50",
|
|
357
357
|
}
|
|
358
|
-
|
|
358
|
+
client.last_response = mock_response
|
|
359
359
|
|
|
360
|
-
result =
|
|
360
|
+
result = client.get_quota_info()
|
|
361
361
|
assert result["limit"] is None
|
|
362
362
|
assert result["remaining"] == 100
|
|
363
363
|
assert result["used"] == 50
|
|
364
364
|
|
|
365
365
|
def test_get_quota_info_invalid_values(self, api_key, base_url):
|
|
366
366
|
"""get_quota_info handles non-numeric values gracefully."""
|
|
367
|
-
|
|
367
|
+
client = ASTRIA(api_key=api_key, base_url=base_url)
|
|
368
368
|
mock_response = Mock()
|
|
369
369
|
mock_response.headers = {
|
|
370
370
|
"X-Quota-Limit": "N/A",
|
|
371
371
|
"X-Quota-Remaining": "remaining",
|
|
372
372
|
"X-Quota-Used": "50",
|
|
373
373
|
}
|
|
374
|
-
|
|
374
|
+
client.last_response = mock_response
|
|
375
375
|
|
|
376
|
-
result =
|
|
376
|
+
result = client.get_quota_info()
|
|
377
377
|
assert result["limit"] is None
|
|
378
378
|
assert result["remaining"] is None
|
|
379
379
|
assert result["used"] == 50
|
|
@@ -1,24 +0,0 @@
|
|
|
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.1"
|
|
17
|
-
__all__ = [
|
|
18
|
-
"BSCE",
|
|
19
|
-
"BSCEError",
|
|
20
|
-
"QuotaExceededError",
|
|
21
|
-
"RateLimitError",
|
|
22
|
-
"AuthenticationError",
|
|
23
|
-
"ValidationError",
|
|
24
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing/utils/pandas_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{subspacecomputing-0.1.1 → subspacecomputing-0.1.4}/subspacecomputing.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|