simudyne-pulse 0.9.0.dev1__tar.gz → 0.9.0.dev3__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.
- {simudyne_pulse-0.9.0.dev1/src/simudyne_pulse.egg-info → simudyne_pulse-0.9.0.dev3}/PKG-INFO +1 -1
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/pyproject.toml +1 -1
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/client.py +89 -12
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/api_keys.py +13 -5
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/data.py +32 -16
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/fix.py +20 -8
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/fm.py +34 -11
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/profile.py +23 -14
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/simulation.py +120 -60
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/validation.py +184 -45
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne_pulse.egg-info/SOURCES.txt +1 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/tests/test_new_resources.py +9 -0
- simudyne_pulse-0.9.0.dev3/tests/test_retries.py +173 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/tests/test_validation.py +75 -2
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/LICENSE +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/README.md +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/setup.cfg +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.9.0.dev1 → simudyne_pulse-0.9.0.dev3}/tests/test_api_contract.py +0 -0
|
@@ -11,6 +11,30 @@ import math
|
|
|
11
11
|
from simudyne.exceptions import PulseAPIError, error_from_response # noqa: F401
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _never_reached_server(error: Exception) -> bool:
|
|
15
|
+
"""Whether a transport error happened before the request was sent.
|
|
16
|
+
|
|
17
|
+
A connect timeout, a refused connection or a failed DNS lookup means the
|
|
18
|
+
API never saw the request, so even a submit is safe to send again. A
|
|
19
|
+
read timeout or a dropped connection may come after the API acted.
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(error, requests.exceptions.ConnectTimeout):
|
|
22
|
+
return True
|
|
23
|
+
if isinstance(error, requests.exceptions.ConnectionError):
|
|
24
|
+
from urllib3.exceptions import NewConnectionError
|
|
25
|
+
|
|
26
|
+
seen = set()
|
|
27
|
+
cause = error
|
|
28
|
+
while cause is not None and id(cause) not in seen:
|
|
29
|
+
seen.add(id(cause))
|
|
30
|
+
if isinstance(cause, NewConnectionError):
|
|
31
|
+
return True
|
|
32
|
+
reason = getattr(cause, "reason", None)
|
|
33
|
+
nested = cause.args[0] if getattr(cause, "args", None) else None
|
|
34
|
+
cause = reason or (nested if isinstance(nested, BaseException) else None) or cause.__cause__
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
14
38
|
class PulseABM:
|
|
15
39
|
"""Client for the Simudyne Pulse synthetic market data API.
|
|
16
40
|
|
|
@@ -19,9 +43,20 @@ class PulseABM:
|
|
|
19
43
|
Construct it once and reuse it; the underlying :class:`requests.Session`
|
|
20
44
|
keeps connections alive across calls.
|
|
21
45
|
|
|
22
|
-
Transient failures
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
Transient failures are retried automatically with exponential backoff,
|
|
47
|
+
but only where sending the request twice is safe. A read (GET, and the
|
|
48
|
+
bulk download) is retried on HTTP 502, 503, 504, timeouts and connection
|
|
49
|
+
errors. A call that creates or changes something — submitting a
|
|
50
|
+
simulation or validation, minting or revoking a key — is retried only on
|
|
51
|
+
503 or when the connection never opened, because a 502, 504 or read
|
|
52
|
+
timeout may arrive after the API acted. A 429 is retried only when it
|
|
53
|
+
carries ``Retry-After``; the download quota's 429 is raised at once. Any
|
|
54
|
+
other non-2xx response, and a retried status still failing once the
|
|
55
|
+
retries run out, raises :class:`~simudyne.exceptions.PulseAPIError`. A timeout or
|
|
56
|
+
connection failure that outlasts the retries is not wrapped: it surfaces
|
|
57
|
+
as the underlying :class:`requests.Timeout` or
|
|
58
|
+
:class:`requests.ConnectionError`, so catch those alongside
|
|
59
|
+
``PulseAPIError`` if you need to handle every failure.
|
|
25
60
|
|
|
26
61
|
Parameters
|
|
27
62
|
----------
|
|
@@ -36,7 +71,8 @@ class PulseABM:
|
|
|
36
71
|
Per-request timeout in seconds. Defaults to 30.
|
|
37
72
|
max_retries : int, default 3
|
|
38
73
|
Retry attempts for transient errors. Backoff is ``2 ** attempt``
|
|
39
|
-
seconds, so the default waits 1s, 2s then 4s before giving up
|
|
74
|
+
seconds, so the default waits 1s, 2s then 4s before giving up; a
|
|
75
|
+
``Retry-After`` header, when sent, is honoured (capped at 60 s).
|
|
40
76
|
|
|
41
77
|
Attributes
|
|
42
78
|
----------
|
|
@@ -83,6 +119,9 @@ class PulseABM:
|
|
|
83
119
|
DEFAULT_BASE_URL = "https://pulse-api.simudyne.com"
|
|
84
120
|
DEFAULT_TIMEOUT = 30
|
|
85
121
|
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
122
|
+
#: Methods safe to send twice. DELETE is left out on purpose: a revoke
|
|
123
|
+
#: that succeeded but whose answer was lost would come back as a 404.
|
|
124
|
+
IDEMPOTENT_METHODS = {"GET", "HEAD", "OPTIONS"}
|
|
86
125
|
|
|
87
126
|
def __init__(
|
|
88
127
|
self,
|
|
@@ -117,9 +156,25 @@ class PulseABM:
|
|
|
117
156
|
self.simulation = SimulationResource(self)
|
|
118
157
|
self.validation = ValidationResource(self)
|
|
119
158
|
|
|
120
|
-
def _request_with_retries(self, method: str, url: str, **kwargs):
|
|
121
|
-
"""Execute request
|
|
159
|
+
def _request_with_retries(self, method: str, url: str, idempotent=None, **kwargs):
|
|
160
|
+
"""Execute a request, retrying only what is safe to send twice.
|
|
161
|
+
|
|
162
|
+
A read (GET, or a POST the caller marks ``idempotent=True``, such as
|
|
163
|
+
the bulk download) is retried on 502/503/504, on a timeout and on a
|
|
164
|
+
connection failure. A call that creates or changes something —
|
|
165
|
+
submitting a simulation or validation, minting or revoking a key,
|
|
166
|
+
launching a live session — is retried only when the API cannot have
|
|
167
|
+
acted on it: the connection never opened, or the API answered 503.
|
|
168
|
+
A 502, 504 or read timeout on such a call may arrive after the job
|
|
169
|
+
was queued, and retrying it used to queue a second one.
|
|
170
|
+
|
|
171
|
+
A 429 is retried only when it carries ``Retry-After``, as a rate
|
|
172
|
+
limiter's does. The API's own 429 is the free-tier download quota,
|
|
173
|
+
which does not clear in seconds; it is raised at once.
|
|
174
|
+
"""
|
|
122
175
|
kwargs.setdefault("timeout", self.timeout)
|
|
176
|
+
if idempotent is None:
|
|
177
|
+
idempotent = method.upper() in self.IDEMPOTENT_METHODS
|
|
123
178
|
last_exception = None
|
|
124
179
|
|
|
125
180
|
for attempt in range(self.max_retries + 1):
|
|
@@ -129,23 +184,45 @@ class PulseABM:
|
|
|
129
184
|
if response.ok:
|
|
130
185
|
return response
|
|
131
186
|
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
187
|
+
if attempt < self.max_retries and self._should_retry(
|
|
188
|
+
response, idempotent
|
|
189
|
+
):
|
|
190
|
+
time.sleep(self._retry_delay(response, attempt))
|
|
135
191
|
continue
|
|
136
192
|
|
|
137
193
|
raise error_from_response(response)
|
|
138
194
|
|
|
139
195
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
|
140
196
|
last_exception = e
|
|
141
|
-
if attempt < self.max_retries
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
if attempt < self.max_retries and (
|
|
198
|
+
idempotent or _never_reached_server(e)
|
|
199
|
+
):
|
|
200
|
+
time.sleep(2 ** attempt)
|
|
144
201
|
continue
|
|
145
202
|
raise
|
|
146
203
|
|
|
147
204
|
raise last_exception
|
|
148
205
|
|
|
206
|
+
def _should_retry(self, response, idempotent: bool) -> bool:
|
|
207
|
+
status = response.status_code
|
|
208
|
+
if status == 429:
|
|
209
|
+
return "Retry-After" in response.headers
|
|
210
|
+
if status not in self.RETRYABLE_STATUS_CODES:
|
|
211
|
+
return False
|
|
212
|
+
return idempotent or status == 503
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _retry_delay(response, attempt: int) -> float:
|
|
216
|
+
"""``Retry-After`` seconds when the server gives them (capped at 60),
|
|
217
|
+
otherwise exponential backoff: 1, 2, 4 s."""
|
|
218
|
+
header = response.headers.get("Retry-After")
|
|
219
|
+
if header is not None:
|
|
220
|
+
try:
|
|
221
|
+
return min(max(float(header), 0.0), 60.0)
|
|
222
|
+
except ValueError:
|
|
223
|
+
pass
|
|
224
|
+
return 2 ** attempt
|
|
225
|
+
|
|
149
226
|
def _request(self, method: str, endpoint: str, **kwargs):
|
|
150
227
|
url = f"{self.base_url}{endpoint}"
|
|
151
228
|
response = self._request_with_retries(method, url, **kwargs)
|
|
@@ -8,6 +8,7 @@ class ApiKeysResource:
|
|
|
8
8
|
--------
|
|
9
9
|
Mint a key for a job, use it, then revoke it:
|
|
10
10
|
|
|
11
|
+
>>> from simudyne import PulseABM
|
|
11
12
|
>>> key = client.api_keys.create(name="Nightly backtest")
|
|
12
13
|
>>> run_client = PulseABM(api_key=key["api_key"])
|
|
13
14
|
>>> client.api_keys.revoke(key_id=key["api_key_id"])
|
|
@@ -46,7 +47,8 @@ class ApiKeysResource:
|
|
|
46
47
|
Raises
|
|
47
48
|
------
|
|
48
49
|
PulseAPIError
|
|
49
|
-
If the
|
|
50
|
+
If the calling key is invalid, revoked or expired — status 401.
|
|
51
|
+
There is no per-account key limit.
|
|
50
52
|
|
|
51
53
|
See Also
|
|
52
54
|
--------
|
|
@@ -65,7 +67,8 @@ class ApiKeysResource:
|
|
|
65
67
|
|
|
66
68
|
The full key is never returned after creation; only ``key_prefix``,
|
|
67
69
|
the first 12 characters, is stored. That is enough to tell keys apart
|
|
68
|
-
and not enough to reconstruct one.
|
|
70
|
+
and not enough to reconstruct one. Revoked keys are not listed, and
|
|
71
|
+
at most 100 keys are returned.
|
|
69
72
|
|
|
70
73
|
Returns
|
|
71
74
|
-------
|
|
@@ -75,7 +78,7 @@ class ApiKeysResource:
|
|
|
75
78
|
- api_key_id (str): the handle to pass to :meth:`revoke`
|
|
76
79
|
- key_prefix (str): first 12 characters, e.g. "pk_live_ab12"
|
|
77
80
|
- name (str): the label you gave it
|
|
78
|
-
- is_active (bool):
|
|
81
|
+
- is_active (bool): always True, since revoked keys are omitted
|
|
79
82
|
- created_at (str): when it was created
|
|
80
83
|
- last_used_at (str or None): None until the key is first used
|
|
81
84
|
- expires_at (str or None): None for keys that do not expire
|
|
@@ -111,12 +114,14 @@ class ApiKeysResource:
|
|
|
111
114
|
Returns
|
|
112
115
|
-------
|
|
113
116
|
dict
|
|
114
|
-
|
|
117
|
+
``{"message": "API key revoked"}``.
|
|
115
118
|
|
|
116
119
|
Raises
|
|
117
120
|
------
|
|
118
121
|
PulseAPIError
|
|
119
|
-
|
|
122
|
+
Status 404 if ``key_id`` does not exist, does not belong to your
|
|
123
|
+
account, or has already been revoked; 401 if the calling key is
|
|
124
|
+
invalid.
|
|
120
125
|
|
|
121
126
|
See Also
|
|
122
127
|
--------
|
|
@@ -131,7 +136,10 @@ class ApiKeysResource:
|
|
|
131
136
|
Revoking the key you are currently authenticating with locks you out
|
|
132
137
|
of the API, so mint the replacement first:
|
|
133
138
|
|
|
139
|
+
>>> from simudyne import PulseABM
|
|
140
|
+
>>> old_key_id = client.api_keys.list()[0]["api_key_id"]
|
|
134
141
|
>>> replacement = client.api_keys.create(name="Rotated")
|
|
142
|
+
>>> client = PulseABM(api_key=replacement["api_key"])
|
|
135
143
|
>>> client.api_keys.revoke(key_id=old_key_id)
|
|
136
144
|
"""
|
|
137
145
|
return self._client._request("DELETE", f"/api-keys/{key_id}")
|
|
@@ -108,9 +108,11 @@ class DataResource:
|
|
|
108
108
|
Raises
|
|
109
109
|
------
|
|
110
110
|
PulseAPIError
|
|
111
|
-
If the key is not pro or demo tier (403)
|
|
112
|
-
model that is not registered (404)
|
|
113
|
-
``YYYY-MM-DD``
|
|
111
|
+
If the key is not pro or demo tier (403); ``model_id`` names a
|
|
112
|
+
model that is not registered (404) or matches more than one
|
|
113
|
+
(400); ``date`` is not ``YYYY-MM-DD``, ``limit`` is outside
|
|
114
|
+
1-200 or ``offset`` is negative (422); or, with ``model_id``, the
|
|
115
|
+
model registry (pulse-orchestrator) could not be reached (502).
|
|
114
116
|
|
|
115
117
|
See Also
|
|
116
118
|
--------
|
|
@@ -189,8 +191,8 @@ class DataResource:
|
|
|
189
191
|
q : str, optional
|
|
190
192
|
Case-insensitive substring match over ticker and company name.
|
|
191
193
|
limit : int, optional
|
|
192
|
-
Max instruments to return. Omitted, the whole calibrated
|
|
193
|
-
comes back — it is far smaller than the registry.
|
|
194
|
+
Max instruments to return, 1-1000. Omitted, the whole calibrated
|
|
195
|
+
catalogue comes back — it is far smaller than the registry.
|
|
194
196
|
offset : int, optional
|
|
195
197
|
Instruments to skip, for paging alongside ``limit``.
|
|
196
198
|
|
|
@@ -215,8 +217,9 @@ class DataResource:
|
|
|
215
217
|
Raises
|
|
216
218
|
------
|
|
217
219
|
PulseAPIError
|
|
218
|
-
If
|
|
219
|
-
``
|
|
220
|
+
If ``limit`` is outside 1-1000 or ``offset`` is negative (422).
|
|
221
|
+
Available on every tier. A malformed ``date`` is not rejected:
|
|
222
|
+
it simply matches nothing, and ``total`` comes back 0.
|
|
220
223
|
|
|
221
224
|
See Also
|
|
222
225
|
--------
|
|
@@ -274,7 +277,7 @@ class DataResource:
|
|
|
274
277
|
:meth:`available_data` and its output is an entry in
|
|
275
278
|
:meth:`calibrated_data`. It runs asynchronously — the call returns once
|
|
276
279
|
the job is queued, and the day appears in :meth:`calibrated_data` when
|
|
277
|
-
the pipeline finishes.
|
|
280
|
+
the pipeline finishes. Demo tier only.
|
|
278
281
|
|
|
279
282
|
A symbol is identified by four fields: ``provider``, ``exchange``,
|
|
280
283
|
``symbol`` and ``cal_date``.
|
|
@@ -299,14 +302,25 @@ class DataResource:
|
|
|
299
302
|
Returns
|
|
300
303
|
-------
|
|
301
304
|
dict
|
|
302
|
-
|
|
303
|
-
|
|
305
|
+
status : str
|
|
306
|
+
"calibration_queued".
|
|
307
|
+
symbol : str
|
|
308
|
+
The symbol as submitted.
|
|
309
|
+
cal_date : str
|
|
310
|
+
The date as submitted.
|
|
311
|
+
|
|
312
|
+
There is no job id: progress is not pollable through the API.
|
|
313
|
+
Watch :meth:`calibrated_data` for the day to appear.
|
|
304
314
|
|
|
305
315
|
Raises
|
|
306
316
|
------
|
|
307
317
|
PulseAPIError
|
|
308
|
-
If the key is not demo tier (403), or
|
|
309
|
-
|
|
318
|
+
If the key is not demo tier (403), ``provider`` or ``exchange``
|
|
319
|
+
is not a recognised value (422), or the calibration service is
|
|
320
|
+
unreachable (503). Missing raw market data for the symbol-day is
|
|
321
|
+
not detected here: the job is queued and fails later in the
|
|
322
|
+
pipeline, so the day simply never appears in
|
|
323
|
+
:meth:`calibrated_data`.
|
|
310
324
|
|
|
311
325
|
See Also
|
|
312
326
|
--------
|
|
@@ -324,17 +338,19 @@ class DataResource:
|
|
|
324
338
|
Examples
|
|
325
339
|
--------
|
|
326
340
|
>>> job = client.data.calibrate(
|
|
327
|
-
... symbol="
|
|
328
|
-
... cal_date="2025-09-
|
|
341
|
+
... symbol="700.HK",
|
|
342
|
+
... cal_date="2025-09-02",
|
|
329
343
|
... provider="omd",
|
|
330
344
|
... exchange="hkex_securities",
|
|
331
345
|
... )
|
|
346
|
+
>>> job["status"]
|
|
347
|
+
'calibration_queued'
|
|
332
348
|
|
|
333
349
|
With an explicit budget, and adjustment-parameter optimisation off:
|
|
334
350
|
|
|
335
351
|
>>> job = client.data.calibrate(
|
|
336
|
-
... symbol="
|
|
337
|
-
... cal_date="2025-09-
|
|
352
|
+
... symbol="700.HK",
|
|
353
|
+
... cal_date="2025-09-02",
|
|
338
354
|
... provider="omd",
|
|
339
355
|
... exchange="hkex_securities",
|
|
340
356
|
... simulations=500,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""FIX venue usage: what you have run over a FIX session.
|
|
2
2
|
|
|
3
|
-
The FIX venue itself is a raw-TCP FIX
|
|
4
|
-
|
|
3
|
+
The FIX venue itself is a raw-TCP FIX 5.0 SP2 acceptor (FIXT.1.1 session
|
|
4
|
+
layer; FIX 4.4 is not accepted), not an HTTP API — you reach it with your own
|
|
5
|
+
FIX engine, not with this SDK. What lives here is the one
|
|
5
6
|
HTTP endpoint a FIX client wants: a record of what it has run.
|
|
6
7
|
|
|
7
8
|
Runs are written by the venue rather than by the client, because the venue is
|
|
@@ -16,8 +17,8 @@ USAGE_PATH = "/fix/usage"
|
|
|
16
17
|
class FixResource:
|
|
17
18
|
"""Usage records for simulations started over the FIX venue.
|
|
18
19
|
|
|
19
|
-
Reached as ``client.fix``. The venue itself is raw-TCP FIX
|
|
20
|
-
driven from this SDK — you connect to it with your own FIX engine. This
|
|
20
|
+
Reached as ``client.fix``. The venue itself is raw-TCP FIX 5.0 SP2 over
|
|
21
|
+
FIXT.1.1 and is not driven from this SDK — you connect to it with your own FIX engine. This
|
|
21
22
|
resource only reads back what the venue recorded.
|
|
22
23
|
|
|
23
24
|
Examples
|
|
@@ -35,14 +36,18 @@ class FixResource:
|
|
|
35
36
|
Parameters
|
|
36
37
|
----------
|
|
37
38
|
user_id : str, optional
|
|
38
|
-
Another account to read, which requires an admin key.
|
|
39
|
-
|
|
39
|
+
Another account to read, which requires an admin key. Omit it
|
|
40
|
+
for your own usage — or, with an admin key, for estate-wide
|
|
41
|
+
totals across every account. An unknown ``user_id`` is not an
|
|
42
|
+
error: it returns an empty summary.
|
|
40
43
|
|
|
41
44
|
Returns
|
|
42
45
|
-------
|
|
43
46
|
dict
|
|
44
47
|
Usage summary containing:
|
|
45
48
|
|
|
49
|
+
- user_id (str or None): the account summarised; None for an
|
|
50
|
+
admin's estate-wide totals
|
|
46
51
|
- total_runs (int): FIX sessions recorded
|
|
47
52
|
- simulated_seconds (float): total horizon across those runs
|
|
48
53
|
- by_symbol / by_scenario / by_cal_date (dict): run counts
|
|
@@ -51,12 +56,13 @@ class FixResource:
|
|
|
51
56
|
- last_run_at (str or None): when the most recent one started,
|
|
52
57
|
None if nothing has been run
|
|
53
58
|
- truncated (bool): True if the history hit the server's cap
|
|
59
|
+
of 5,000 runs, in which case only the newest 5,000 are counted
|
|
54
60
|
|
|
55
61
|
Raises
|
|
56
62
|
------
|
|
57
63
|
PulseAPIError
|
|
58
|
-
If ``user_id``
|
|
59
|
-
|
|
64
|
+
If ``user_id`` names another account and the key is not an admin
|
|
65
|
+
key (403), or the key is invalid, revoked or expired (401).
|
|
60
66
|
|
|
61
67
|
See Also
|
|
62
68
|
--------
|
|
@@ -74,6 +80,12 @@ class FixResource:
|
|
|
74
80
|
Read another account, which needs an admin key:
|
|
75
81
|
|
|
76
82
|
>>> client.fix.usage(user_id="usr_01H...")
|
|
83
|
+
|
|
84
|
+
With an admin key and no ``user_id``, the whole estate:
|
|
85
|
+
|
|
86
|
+
>>> estate = client.fix.usage()
|
|
87
|
+
>>> estate["user_id"] is None
|
|
88
|
+
True
|
|
77
89
|
"""
|
|
78
90
|
params = {"user_id": user_id} if user_id else None
|
|
79
91
|
return self._client._request("GET", USAGE_PATH, params=params)
|
|
@@ -32,7 +32,11 @@ class FmResource:
|
|
|
32
32
|
model accepts, then submit through ``client.simulation.run(engine="fm")``
|
|
33
33
|
like any other simulation.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
:meth:`models` and :meth:`live` require a pro- or demo-tier key.
|
|
36
|
+
:meth:`registry`, :meth:`register`, :meth:`activate` and
|
|
37
|
+
:meth:`deactivate` require a key whose account has the admin flag
|
|
38
|
+
(``is_admin`` in :meth:`~simudyne.resources.profile.ProfileResource.get`);
|
|
39
|
+
tier plays no part in those four.
|
|
36
40
|
|
|
37
41
|
Examples
|
|
38
42
|
--------
|
|
@@ -61,7 +65,7 @@ class FmResource:
|
|
|
61
65
|
def models(self):
|
|
62
66
|
"""Foundation models active in this environment.
|
|
63
67
|
|
|
64
|
-
Requires a pro-tier key.
|
|
68
|
+
Requires a pro- or demo-tier key.
|
|
65
69
|
|
|
66
70
|
Returns
|
|
67
71
|
-------
|
|
@@ -84,7 +88,8 @@ class FmResource:
|
|
|
84
88
|
Raises
|
|
85
89
|
------
|
|
86
90
|
PulseAPIError
|
|
87
|
-
If the key is not pro tier
|
|
91
|
+
If the key is not pro or demo tier (403), or the model registry
|
|
92
|
+
(pulse-orchestrator) cannot be reached (502).
|
|
88
93
|
|
|
89
94
|
See Also
|
|
90
95
|
--------
|
|
@@ -124,11 +129,12 @@ class FmResource:
|
|
|
124
129
|
Raises
|
|
125
130
|
------
|
|
126
131
|
PulseAPIError
|
|
127
|
-
If the key
|
|
132
|
+
If the key's account lacks the admin flag (403), or the
|
|
133
|
+
orchestrator cannot be reached (502).
|
|
128
134
|
|
|
129
135
|
See Also
|
|
130
136
|
--------
|
|
131
|
-
models : The active models only, for any pro key.
|
|
137
|
+
models : The active models only, for any pro or demo key.
|
|
132
138
|
register : Add a model or a new version of one.
|
|
133
139
|
|
|
134
140
|
Examples
|
|
@@ -148,6 +154,7 @@ class FmResource:
|
|
|
148
154
|
supported_data: list | None = None,
|
|
149
155
|
min_prompt_rows: int | None = None,
|
|
150
156
|
production_name: str = "",
|
|
157
|
+
testdata: dict | None = None,
|
|
151
158
|
active: bool = True,
|
|
152
159
|
):
|
|
153
160
|
"""Admin: register a model image, or a new version of one.
|
|
@@ -178,6 +185,11 @@ class FmResource:
|
|
|
178
185
|
Context rows it needs to start inference.
|
|
179
186
|
production_name : str, default ""
|
|
180
187
|
The name to promote it under, ``{model}-{market}-{version}-{size}``.
|
|
188
|
+
testdata : dict, optional
|
|
189
|
+
The real day baked into the image as its test data,
|
|
190
|
+
``{"provider", "exchange", "symbol", "date"}``. Test-data runs are
|
|
191
|
+
minted under that day, so their sim_ids name it instead of
|
|
192
|
+
``na:na:testdata:na``.
|
|
181
193
|
active : bool, default True
|
|
182
194
|
Whether :meth:`models` lists it.
|
|
183
195
|
|
|
@@ -190,8 +202,12 @@ class FmResource:
|
|
|
190
202
|
Raises
|
|
191
203
|
------
|
|
192
204
|
PulseAPIError
|
|
193
|
-
If the key
|
|
194
|
-
or
|
|
205
|
+
If the key's account lacks the admin flag (403); ``image`` is not
|
|
206
|
+
found in its container registry, or ``model_args`` or
|
|
207
|
+
``supported_data`` is malformed (400); a field has the wrong type
|
|
208
|
+
(422); the deployment's registry is a read-only manifest, so
|
|
209
|
+
models cannot be registered through the API (501); or the
|
|
210
|
+
orchestrator, or the image check, cannot be reached (502).
|
|
195
211
|
|
|
196
212
|
See Also
|
|
197
213
|
--------
|
|
@@ -217,6 +233,7 @@ class FmResource:
|
|
|
217
233
|
for name, value in (
|
|
218
234
|
("resources", resources), ("model_args", model_args),
|
|
219
235
|
("supported_data", supported_data), ("min_prompt_rows", min_prompt_rows),
|
|
236
|
+
("testdata", testdata),
|
|
220
237
|
):
|
|
221
238
|
if value is not None:
|
|
222
239
|
payload[name] = value
|
|
@@ -239,7 +256,9 @@ class FmResource:
|
|
|
239
256
|
Raises
|
|
240
257
|
------
|
|
241
258
|
PulseAPIError
|
|
242
|
-
If the key
|
|
259
|
+
If the key's account lacks the admin flag (403), the model is
|
|
260
|
+
unknown (404), the deployment's registry is a read-only manifest
|
|
261
|
+
(501), or the orchestrator cannot be reached (502).
|
|
243
262
|
|
|
244
263
|
See Also
|
|
245
264
|
--------
|
|
@@ -270,7 +289,9 @@ class FmResource:
|
|
|
270
289
|
Raises
|
|
271
290
|
------
|
|
272
291
|
PulseAPIError
|
|
273
|
-
If the key
|
|
292
|
+
If the key's account lacks the admin flag (403), the model is
|
|
293
|
+
unknown (404), the deployment's registry is a read-only manifest
|
|
294
|
+
(501), or the orchestrator cannot be reached (502).
|
|
274
295
|
|
|
275
296
|
See Also
|
|
276
297
|
--------
|
|
@@ -293,7 +314,7 @@ class FmResource:
|
|
|
293
314
|
"""Start a live streaming session instead of a batch job.
|
|
294
315
|
|
|
295
316
|
Returns the token a live chart client uses to connect. Requires a
|
|
296
|
-
pro-tier key.
|
|
317
|
+
pro- or demo-tier key.
|
|
297
318
|
|
|
298
319
|
Parameters
|
|
299
320
|
----------
|
|
@@ -316,7 +337,9 @@ class FmResource:
|
|
|
316
337
|
------
|
|
317
338
|
PulseAPIError
|
|
318
339
|
If ``model_id`` is unknown (404), ``model_args`` holds an unknown
|
|
319
|
-
or out-of-range key (400),
|
|
340
|
+
or out-of-range key (400), the key is not pro or demo tier (403),
|
|
341
|
+
this deployment cannot host live sessions (501), or the
|
|
342
|
+
orchestrator cannot be reached (502).
|
|
320
343
|
|
|
321
344
|
See Also
|
|
322
345
|
--------
|
|
@@ -70,7 +70,13 @@ class ProfileResource:
|
|
|
70
70
|
"""Return your request count for the current calendar month.
|
|
71
71
|
|
|
72
72
|
Resets on the 1st, and counts *requests* rather than simulations —
|
|
73
|
-
one ``run()`` of 100 Monte Carlo runs is a single request here.
|
|
73
|
+
one ``run()`` of 100 Monte Carlo runs is a single request here. Only
|
|
74
|
+
the routes that log usage are counted: the data routes, the
|
|
75
|
+
simulation routes (run, LRM run, jobs and their status, results and
|
|
76
|
+
logs, per-sim files, params, metrics and data, bulk download, cached
|
|
77
|
+
and sample), validation ``run`` and ``run/upload``, FM ``live`` and
|
|
78
|
+
the historical routes. Profile, API-key, validation job reads, FM
|
|
79
|
+
model management and FIX usage calls are not counted.
|
|
74
80
|
|
|
75
81
|
Returns
|
|
76
82
|
-------
|
|
@@ -82,10 +88,10 @@ class ProfileResource:
|
|
|
82
88
|
total_requests : int
|
|
83
89
|
Requests so far this month.
|
|
84
90
|
by_endpoint : dict
|
|
85
|
-
Request count keyed by
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
Request count keyed by route label. Job- and sim-scoped
|
|
92
|
+
routes are logged under their template, e.g.
|
|
93
|
+
``"/simulation/jobs/{job_id}/status"``, not once per concrete
|
|
94
|
+
job id, so each key is already a per-endpoint total.
|
|
89
95
|
|
|
90
96
|
Raises
|
|
91
97
|
------
|
|
@@ -102,12 +108,13 @@ class ProfileResource:
|
|
|
102
108
|
>>> usage = client.profile.usage()
|
|
103
109
|
>>> print(f"{usage['month']}: {usage['total_requests']} requests")
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
Roll the per-route counts up into per-area totals:
|
|
106
112
|
|
|
107
113
|
>>> from collections import Counter
|
|
108
114
|
>>> totals = Counter()
|
|
109
|
-
>>> for
|
|
110
|
-
... totals[
|
|
115
|
+
>>> for route, count in usage["by_endpoint"].items():
|
|
116
|
+
... totals[route.split("/")[1]] += count
|
|
117
|
+
>>> print(totals.most_common())
|
|
111
118
|
"""
|
|
112
119
|
return self._client._request("GET", "/profile/usage")
|
|
113
120
|
|
|
@@ -134,7 +141,14 @@ class ProfileResource:
|
|
|
134
141
|
downloaded_groups : list of str or None
|
|
135
142
|
Every group you have ever downloaded. None when unlimited.
|
|
136
143
|
downloaded_group_details : list of dict or None
|
|
137
|
-
The same groups, each
|
|
144
|
+
The same groups, newest first, each a dict with
|
|
145
|
+
``job_key`` (str, the group, as in ``downloaded_groups``)
|
|
146
|
+
and ``downloaded_at`` (ISO timestamp str, or None).
|
|
147
|
+
|
|
148
|
+
Raises
|
|
149
|
+
------
|
|
150
|
+
PulseAPIError
|
|
151
|
+
If the key is invalid, revoked or expired — status 401.
|
|
138
152
|
|
|
139
153
|
See Also
|
|
140
154
|
--------
|
|
@@ -147,11 +161,6 @@ class ProfileResource:
|
|
|
147
161
|
A request that would exceed the allowance returns HTTP 429 without
|
|
148
162
|
charging anything, so a partial download never silently eats quota.
|
|
149
163
|
|
|
150
|
-
Raises
|
|
151
|
-
------
|
|
152
|
-
PulseAPIError
|
|
153
|
-
If the key is invalid, revoked or expired — status 401.
|
|
154
|
-
|
|
155
164
|
Examples
|
|
156
165
|
--------
|
|
157
166
|
>>> quota = client.profile.downloads()
|