simudyne-pulse 0.7.2__tar.gz → 0.8.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. simudyne_pulse-0.8.0.dev1/PKG-INFO +270 -0
  2. simudyne_pulse-0.8.0.dev1/README.md +244 -0
  3. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/pyproject.toml +2 -2
  4. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/client.py +82 -6
  5. simudyne_pulse-0.8.0.dev1/src/simudyne/exceptions.py +96 -0
  6. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/api_keys.py +57 -3
  7. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/data.py +356 -0
  8. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/fix.py +79 -0
  9. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/fm.py +340 -0
  10. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/profile.py +62 -0
  11. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/simulation.py +1292 -0
  12. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/simulator_gym.py +304 -0
  13. simudyne_pulse-0.8.0.dev1/src/simudyne/resources/validation.py +677 -0
  14. simudyne_pulse-0.8.0.dev1/src/simudyne_pulse.egg-info/PKG-INFO +270 -0
  15. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/SOURCES.txt +6 -2
  16. simudyne_pulse-0.8.0.dev1/tests/test_api_contract.py +191 -0
  17. simudyne_pulse-0.8.0.dev1/tests/test_new_resources.py +214 -0
  18. simudyne_pulse-0.8.0.dev1/tests/test_validation.py +429 -0
  19. simudyne_pulse-0.7.2/PKG-INFO +0 -147
  20. simudyne_pulse-0.7.2/README.md +0 -121
  21. simudyne_pulse-0.7.2/src/simudyne/exceptions.py +0 -7
  22. simudyne_pulse-0.7.2/src/simudyne/resources/data.py +0 -53
  23. simudyne_pulse-0.7.2/src/simudyne/resources/historical.py +0 -1
  24. simudyne_pulse-0.7.2/src/simudyne/resources/simulation.py +0 -790
  25. simudyne_pulse-0.7.2/src/simudyne/resources/simulator_gym.py +0 -131
  26. simudyne_pulse-0.7.2/src/simudyne/resources/validation.py +0 -299
  27. simudyne_pulse-0.7.2/src/simudyne_pulse.egg-info/PKG-INFO +0 -147
  28. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/LICENSE +0 -0
  29. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/setup.cfg +0 -0
  30. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/__init__.py +0 -0
  31. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/__init__.py +0 -0
  32. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  33. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  34. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  35. {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/tests/test_simulator_gym.py +0 -0
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: simudyne-pulse
3
+ Version: 0.8.0.dev1
4
+ Summary: Python SDK for the Simudyne Pulse synthetic market data API
5
+ Author-email: Simudyne <support@simudyne.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://pulse.simudyne.com
8
+ Project-URL: Documentation, https://pulse.simudyne.com/docs
9
+ Project-URL: Repository, https://github.com/simudyne/pulse-sdk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: polars>=0.20.0
21
+ Requires-Dist: tqdm>=4.60.0
22
+ Requires-Dist: websocket-client>=1.0.0
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=8.0.0; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # Simudyne Pulse Python SDK
28
+
29
+ Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install "simudyne-pulse>=0.8.0"
35
+ ```
36
+
37
+ 0.7.x predates the current API: its validation and data calls are rejected by
38
+ pulse-api-pod 1.77 and later.
39
+
40
+ The distribution is named `simudyne-pulse`; the import name is `simudyne`:
41
+
42
+ ```python
43
+ from simudyne import PulseABM
44
+ ```
45
+
46
+ Requires Python 3.10+.
47
+
48
+ ### Development builds
49
+
50
+ The `dev` branch is a prerelease channel. Pushes to it publish prerelease
51
+ versions (e.g. `0.8.0.dev1`) that are separate from the stable versions cut on
52
+ `prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
53
+ release and ignores prereleases, so dev builds can never affect a normal
54
+ install.
55
+
56
+ To install the latest dev build, opt in with `--pre`:
57
+
58
+ ```bash
59
+ pip install --pre simudyne-pulse
60
+ ```
61
+
62
+ Only use dev builds for testing unreleased changes; they are not guaranteed
63
+ stable. Merge `dev` into `prod` to promote those changes to a stable release.
64
+
65
+ ### Comparing named populations
66
+
67
+ Simulations are often not one population but several — a foundation model and
68
+ an ABM, or two versions of a model. Name them and the results compare them
69
+ instead of averaging them together:
70
+
71
+ ```python
72
+ job = client.validation.run(
73
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
74
+ sim_ids={"fm": fm_ids, "abm": abm_ids},
75
+ plots=["statistical.radar", "stylised_facts.overall",
76
+ "volume_correlation.overall"],
77
+ )
78
+ result = client.validation.get_job(job["job_id"], plot_dir="figs")
79
+ result["distances"]["fm"]["spread"]["l1"]
80
+ result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
81
+ ```
82
+
83
+ `distances`, `distributions`, the verdicts and the FID/MIND scores come back
84
+ keyed by name; the radar draws a polygon per population, the verdict table a
85
+ column each, and volume correlation puts historical first then a heatmap per
86
+ population. `sim_files` groups the same way.
87
+
88
+ A plain list is one unnamed population and keeps the flat shape exactly as
89
+ before. The historical day is measured once however many groups there are.
90
+
91
+ ### Your own runs beside platform runs
92
+
93
+ `sim_ids` and `sim_files` can be given together — your own model against
94
+ platform simulations, on one set of figures, 25 runs in total across the two:
95
+
96
+ ```python
97
+ job = client.validation.run(
98
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
99
+ sim_files={"fm": ["my_run.parquet"]},
100
+ sim_ids={"abm": abm_ids},
101
+ plots=["statistical.radar", "stylised_facts.overall"],
102
+ )
103
+ ```
104
+
105
+ With both present each source is a population of its own, so a bare list gets
106
+ a name rather than merging into whatever else is there:
107
+
108
+ | passed | populations |
109
+ | --- | --- |
110
+ | files mapping + ids list | its names, plus `platform` |
111
+ | files list + ids mapping | `uploaded`, plus its names |
112
+ | both lists | `uploaded` and `platform` |
113
+ | both mappings | their own names |
114
+ | either one alone | exactly as before |
115
+
116
+ The uploads are measured first and the platform runs follow.
117
+
118
+ `plots` is the only plot switch: unset draws nothing, `True` draws every
119
+ figure the enabled areas can draw, and a list draws just those ids. An area
120
+ switched off draws nothing either way.
121
+
122
+ | id | figure |
123
+ | --- | --- |
124
+ | `statistical.radar` | distance spider, one polygon per population |
125
+ | `statistical.distribution` | every metric's KDE |
126
+ | `statistical.distribution.{metric}` | one metric, e.g. `.spread` |
127
+ | `stylised_facts.overall` | the verdict table |
128
+ | `stylised_facts.fact` | every fact |
129
+ | `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
130
+ | `impact.response` | impact response by event type |
131
+ | `impact.event` | every event type |
132
+ | `impact.event.{type}` | one event type |
133
+ | `volume_correlation.levels` | level correlation heatmap |
134
+ | `volume_correlation.changes` | change correlation heatmap |
135
+ | `volume_correlation.overall` | both of the above |
136
+
137
+ Each returned figure carries the `id` it was asked for — match on that, not on
138
+ the filename.
139
+
140
+ ### Working from a checkout
141
+
142
+ To run the SDK from source — editing it, or using unreleased changes — install
143
+ it editable **into the interpreter you will actually import from**. A notebook
144
+ kernel is frequently not the python on your `$PATH`:
145
+
146
+ ```bash
147
+ # in a notebook, find the right interpreter first
148
+ import sys; print(sys.executable)
149
+
150
+ # then, with that path
151
+ <that python> -m pip install -e /path/to/pulse-sdk
152
+ ```
153
+
154
+ Check which copy you loaded — this is worth doing whenever an attribute seems
155
+ to be missing:
156
+
157
+ ```python
158
+ import simudyne
159
+ from importlib.metadata import version
160
+
161
+ print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
162
+ print(version("simudyne-pulse"))
163
+ ```
164
+
165
+ #### If an older install shadows it
166
+
167
+ The SDK was once published under the distribution name **`simudyne`**; it is
168
+ now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
169
+ one installs a real directory while the editable install only adds a path
170
+ entry — so the old copy wins and you get errors like
171
+ `'PulseABM' object has no attribute 'validation'` from a version that predates
172
+ the feature. `pip list` shows both. Remove the obsolete one:
173
+
174
+ ```bash
175
+ <that python> -m pip uninstall simudyne # the old distribution
176
+ <that python> -m pip install -e /path/to/pulse-sdk
177
+ ```
178
+
179
+ ### Running the tests
180
+
181
+ ```bash
182
+ uv run --with pytest --with requests --with pandas --with pyarrow \
183
+ python -m pytest tests/ -q
184
+ ```
185
+
186
+ No network: the client's `_request` is replaced with a recorder, so the tests
187
+ assert the exact payload the API would receive.
188
+
189
+ ## Quick start
190
+
191
+ ```python
192
+ from simudyne import PulseABM
193
+
194
+ client = PulseABM(api_key="pk_live_...") # or set SIMUDYNE_API_KEY
195
+
196
+ # Days the agent-based model can simulate
197
+ cal = client.data.calibrated_data(symbol="700.HK")
198
+
199
+ # Run it, poll, read the generated book
200
+ job = client.simulation.run(
201
+ symbol="700.HK", cal_date="2025-09-02",
202
+ provider="omd", exchange="hkex_securities",
203
+ )
204
+ import time
205
+ while not client.simulation.get_job_status(job["job_id"])["is_complete"]:
206
+ time.sleep(20)
207
+ book = client.simulation.get_sim_data(job["sim_ids"][0])
208
+ ```
209
+
210
+ A foundation model runs through the same call with `engine="fm"` and a
211
+ `model_id`, which may be the model's production name (e.g.
212
+ `"flow-hkex-1-100M"`; see `client.fm.models()`). Fields that belong to the
213
+ other engine are sent and rejected by the API with a 422 rather than dropped.
214
+
215
+ Every method has a full numpy-style docstring (`help(client.simulation.run)`);
216
+ the user guide is at https://pulse.simudyne.com/docs.
217
+
218
+ ## API reference
219
+
220
+ ### `PulseABM(api_key, base_url=None)`
221
+
222
+ | Parameter | Env variable | Default |
223
+ |-----------|-------------|---------|
224
+ | `api_key` | `SIMUDYNE_API_KEY` | required |
225
+ | `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
226
+
227
+ | Resource | What it covers |
228
+ |----------|----------------|
229
+ | `client.data` | `available_data` (every symbol-day that exists), `calibrated_data` (what the ABM can run), `calibrate` |
230
+ | `client.simulation` | `run` (both engines), `run_lrm`, `get_jobs`, `get_job_status`, `get_job_results`, `get_job_logs`, sim files and downloads |
231
+ | `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
232
+ | `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
233
+ | `client.fix` | FIX session usage |
234
+ | `client.simulator_gym` | the interactive simulator websocket |
235
+
236
+ Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
237
+ API sent it) and `errors`; `str(exc)` renders validation errors as
238
+ `field: message` lines.
239
+
240
+ ### `client.profile`
241
+
242
+ ```python
243
+ client.profile.get() # Account info
244
+ client.profile.usage() # API usage stats
245
+ ```
246
+
247
+ ### `client.api_keys`
248
+
249
+ ```python
250
+ client.api_keys.list() # List active keys
251
+ client.api_keys.create(name="research") # Create a new key
252
+ client.api_keys.revoke(key_id="key_...") # Revoke a key
253
+ ```
254
+
255
+ ## Configuration
256
+
257
+ You can set your API key as an environment variable instead of passing it directly:
258
+
259
+ ```bash
260
+ export SIMUDYNE_API_KEY=pk_live_...
261
+ ```
262
+
263
+ ```python
264
+ from simudyne import PulseABM
265
+ client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
266
+ ```
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,244 @@
1
+ # Simudyne Pulse Python SDK
2
+
3
+ Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install "simudyne-pulse>=0.8.0"
9
+ ```
10
+
11
+ 0.7.x predates the current API: its validation and data calls are rejected by
12
+ pulse-api-pod 1.77 and later.
13
+
14
+ The distribution is named `simudyne-pulse`; the import name is `simudyne`:
15
+
16
+ ```python
17
+ from simudyne import PulseABM
18
+ ```
19
+
20
+ Requires Python 3.10+.
21
+
22
+ ### Development builds
23
+
24
+ The `dev` branch is a prerelease channel. Pushes to it publish prerelease
25
+ versions (e.g. `0.8.0.dev1`) that are separate from the stable versions cut on
26
+ `prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
27
+ release and ignores prereleases, so dev builds can never affect a normal
28
+ install.
29
+
30
+ To install the latest dev build, opt in with `--pre`:
31
+
32
+ ```bash
33
+ pip install --pre simudyne-pulse
34
+ ```
35
+
36
+ Only use dev builds for testing unreleased changes; they are not guaranteed
37
+ stable. Merge `dev` into `prod` to promote those changes to a stable release.
38
+
39
+ ### Comparing named populations
40
+
41
+ Simulations are often not one population but several — a foundation model and
42
+ an ABM, or two versions of a model. Name them and the results compare them
43
+ instead of averaging them together:
44
+
45
+ ```python
46
+ job = client.validation.run(
47
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
48
+ sim_ids={"fm": fm_ids, "abm": abm_ids},
49
+ plots=["statistical.radar", "stylised_facts.overall",
50
+ "volume_correlation.overall"],
51
+ )
52
+ result = client.validation.get_job(job["job_id"], plot_dir="figs")
53
+ result["distances"]["fm"]["spread"]["l1"]
54
+ result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
55
+ ```
56
+
57
+ `distances`, `distributions`, the verdicts and the FID/MIND scores come back
58
+ keyed by name; the radar draws a polygon per population, the verdict table a
59
+ column each, and volume correlation puts historical first then a heatmap per
60
+ population. `sim_files` groups the same way.
61
+
62
+ A plain list is one unnamed population and keeps the flat shape exactly as
63
+ before. The historical day is measured once however many groups there are.
64
+
65
+ ### Your own runs beside platform runs
66
+
67
+ `sim_ids` and `sim_files` can be given together — your own model against
68
+ platform simulations, on one set of figures, 25 runs in total across the two:
69
+
70
+ ```python
71
+ job = client.validation.run(
72
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
73
+ sim_files={"fm": ["my_run.parquet"]},
74
+ sim_ids={"abm": abm_ids},
75
+ plots=["statistical.radar", "stylised_facts.overall"],
76
+ )
77
+ ```
78
+
79
+ With both present each source is a population of its own, so a bare list gets
80
+ a name rather than merging into whatever else is there:
81
+
82
+ | passed | populations |
83
+ | --- | --- |
84
+ | files mapping + ids list | its names, plus `platform` |
85
+ | files list + ids mapping | `uploaded`, plus its names |
86
+ | both lists | `uploaded` and `platform` |
87
+ | both mappings | their own names |
88
+ | either one alone | exactly as before |
89
+
90
+ The uploads are measured first and the platform runs follow.
91
+
92
+ `plots` is the only plot switch: unset draws nothing, `True` draws every
93
+ figure the enabled areas can draw, and a list draws just those ids. An area
94
+ switched off draws nothing either way.
95
+
96
+ | id | figure |
97
+ | --- | --- |
98
+ | `statistical.radar` | distance spider, one polygon per population |
99
+ | `statistical.distribution` | every metric's KDE |
100
+ | `statistical.distribution.{metric}` | one metric, e.g. `.spread` |
101
+ | `stylised_facts.overall` | the verdict table |
102
+ | `stylised_facts.fact` | every fact |
103
+ | `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
104
+ | `impact.response` | impact response by event type |
105
+ | `impact.event` | every event type |
106
+ | `impact.event.{type}` | one event type |
107
+ | `volume_correlation.levels` | level correlation heatmap |
108
+ | `volume_correlation.changes` | change correlation heatmap |
109
+ | `volume_correlation.overall` | both of the above |
110
+
111
+ Each returned figure carries the `id` it was asked for — match on that, not on
112
+ the filename.
113
+
114
+ ### Working from a checkout
115
+
116
+ To run the SDK from source — editing it, or using unreleased changes — install
117
+ it editable **into the interpreter you will actually import from**. A notebook
118
+ kernel is frequently not the python on your `$PATH`:
119
+
120
+ ```bash
121
+ # in a notebook, find the right interpreter first
122
+ import sys; print(sys.executable)
123
+
124
+ # then, with that path
125
+ <that python> -m pip install -e /path/to/pulse-sdk
126
+ ```
127
+
128
+ Check which copy you loaded — this is worth doing whenever an attribute seems
129
+ to be missing:
130
+
131
+ ```python
132
+ import simudyne
133
+ from importlib.metadata import version
134
+
135
+ print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
136
+ print(version("simudyne-pulse"))
137
+ ```
138
+
139
+ #### If an older install shadows it
140
+
141
+ The SDK was once published under the distribution name **`simudyne`**; it is
142
+ now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
143
+ one installs a real directory while the editable install only adds a path
144
+ entry — so the old copy wins and you get errors like
145
+ `'PulseABM' object has no attribute 'validation'` from a version that predates
146
+ the feature. `pip list` shows both. Remove the obsolete one:
147
+
148
+ ```bash
149
+ <that python> -m pip uninstall simudyne # the old distribution
150
+ <that python> -m pip install -e /path/to/pulse-sdk
151
+ ```
152
+
153
+ ### Running the tests
154
+
155
+ ```bash
156
+ uv run --with pytest --with requests --with pandas --with pyarrow \
157
+ python -m pytest tests/ -q
158
+ ```
159
+
160
+ No network: the client's `_request` is replaced with a recorder, so the tests
161
+ assert the exact payload the API would receive.
162
+
163
+ ## Quick start
164
+
165
+ ```python
166
+ from simudyne import PulseABM
167
+
168
+ client = PulseABM(api_key="pk_live_...") # or set SIMUDYNE_API_KEY
169
+
170
+ # Days the agent-based model can simulate
171
+ cal = client.data.calibrated_data(symbol="700.HK")
172
+
173
+ # Run it, poll, read the generated book
174
+ job = client.simulation.run(
175
+ symbol="700.HK", cal_date="2025-09-02",
176
+ provider="omd", exchange="hkex_securities",
177
+ )
178
+ import time
179
+ while not client.simulation.get_job_status(job["job_id"])["is_complete"]:
180
+ time.sleep(20)
181
+ book = client.simulation.get_sim_data(job["sim_ids"][0])
182
+ ```
183
+
184
+ A foundation model runs through the same call with `engine="fm"` and a
185
+ `model_id`, which may be the model's production name (e.g.
186
+ `"flow-hkex-1-100M"`; see `client.fm.models()`). Fields that belong to the
187
+ other engine are sent and rejected by the API with a 422 rather than dropped.
188
+
189
+ Every method has a full numpy-style docstring (`help(client.simulation.run)`);
190
+ the user guide is at https://pulse.simudyne.com/docs.
191
+
192
+ ## API reference
193
+
194
+ ### `PulseABM(api_key, base_url=None)`
195
+
196
+ | Parameter | Env variable | Default |
197
+ |-----------|-------------|---------|
198
+ | `api_key` | `SIMUDYNE_API_KEY` | required |
199
+ | `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
200
+
201
+ | Resource | What it covers |
202
+ |----------|----------------|
203
+ | `client.data` | `available_data` (every symbol-day that exists), `calibrated_data` (what the ABM can run), `calibrate` |
204
+ | `client.simulation` | `run` (both engines), `run_lrm`, `get_jobs`, `get_job_status`, `get_job_results`, `get_job_logs`, sim files and downloads |
205
+ | `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
206
+ | `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
207
+ | `client.fix` | FIX session usage |
208
+ | `client.simulator_gym` | the interactive simulator websocket |
209
+
210
+ Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
211
+ API sent it) and `errors`; `str(exc)` renders validation errors as
212
+ `field: message` lines.
213
+
214
+ ### `client.profile`
215
+
216
+ ```python
217
+ client.profile.get() # Account info
218
+ client.profile.usage() # API usage stats
219
+ ```
220
+
221
+ ### `client.api_keys`
222
+
223
+ ```python
224
+ client.api_keys.list() # List active keys
225
+ client.api_keys.create(name="research") # Create a new key
226
+ client.api_keys.revoke(key_id="key_...") # Revoke a key
227
+ ```
228
+
229
+ ## Configuration
230
+
231
+ You can set your API key as an environment variable instead of passing it directly:
232
+
233
+ ```bash
234
+ export SIMUDYNE_API_KEY=pk_live_...
235
+ ```
236
+
237
+ ```python
238
+ from simudyne import PulseABM
239
+ client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
240
+ ```
241
+
242
+ ## License
243
+
244
+ MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simudyne-pulse"
7
- version = "0.7.2"
7
+ version = "0.8.0-dev.1"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -33,7 +33,7 @@ test = ["pytest>=8.0.0"]
33
33
  [project.urls]
34
34
  Homepage = "https://pulse.simudyne.com"
35
35
  Documentation = "https://pulse.simudyne.com/docs"
36
- Repository = "https://github.com/simudyne/pulse-api"
36
+ Repository = "https://github.com/simudyne/pulse-sdk"
37
37
 
38
38
 
39
39
 
@@ -8,10 +8,80 @@ from tqdm import tqdm
8
8
  import tempfile
9
9
  import math
10
10
 
11
- from simudyne.exceptions import PulseAPIError
11
+ from simudyne.exceptions import PulseAPIError, error_from_response # noqa: F401
12
12
 
13
13
 
14
14
  class PulseABM:
15
+ """Client for the Simudyne Pulse synthetic market data API.
16
+
17
+ The client holds the API key and HTTP session, and exposes every endpoint
18
+ through a resource attribute (``client.simulation``, ``client.data``, ...).
19
+ Construct it once and reuse it; the underlying :class:`requests.Session`
20
+ keeps connections alive across calls.
21
+
22
+ Transient failures (HTTP 429, 502, 503, 504) and network timeouts are
23
+ retried automatically with exponential backoff. Any other non-2xx response
24
+ raises :class:`~simudyne.exceptions.PulseAPIError`.
25
+
26
+ Parameters
27
+ ----------
28
+ api_key : str, optional
29
+ Pulse API key (``pk_live_...``). Falls back to the ``SIMUDYNE_API_KEY``
30
+ environment variable when omitted.
31
+ base_url : str, optional
32
+ API root, with no trailing slash. Falls back to the
33
+ ``SIMUDYNE_BASE_URL`` environment variable, then to
34
+ ``https://pulse-api.simudyne.com``.
35
+ timeout : int, optional
36
+ Per-request timeout in seconds. Defaults to 30.
37
+ max_retries : int, default 3
38
+ Retry attempts for transient errors. Backoff is ``2 ** attempt``
39
+ seconds, so the default waits 1s, 2s then 4s before giving up.
40
+
41
+ Attributes
42
+ ----------
43
+ profile : ProfileResource
44
+ Account details, usage and download history.
45
+ api_keys : ApiKeysResource
46
+ Create, list and revoke API keys.
47
+ data : DataResource
48
+ The market data catalogue: what exists, what is calibrated,
49
+ and calibrating a day.
50
+ simulation : SimulationResource
51
+ Submit agent-based simulations and retrieve their results.
52
+ validation : ValidationResource
53
+ Score synthetic data against real data.
54
+ fm : FmResource
55
+ Foundation-model generation and its job lifecycle.
56
+ fix : FixResource
57
+ FIX gateway usage.
58
+ simulator_gym : SimulatorGymResource
59
+ Gym-style reinforcement-learning environment over a WebSocket.
60
+
61
+ Raises
62
+ ------
63
+ ValueError
64
+ If no API key is given and ``SIMUDYNE_API_KEY`` is unset.
65
+
66
+ Examples
67
+ --------
68
+ Key taken from the environment:
69
+
70
+ >>> import os
71
+ >>> os.environ["SIMUDYNE_API_KEY"] = "pk_live_..."
72
+ >>> client = PulseABM()
73
+
74
+ Key passed explicitly, against a non-default deployment:
75
+
76
+ >>> client = PulseABM(
77
+ ... api_key="pk_live_...",
78
+ ... base_url="https://pulse-api-dev.simudyne.com",
79
+ ... timeout=60,
80
+ ... )
81
+ >>> client.profile.get()["email"]
82
+ 'you@example.com'
83
+ """
84
+
15
85
  DEFAULT_BASE_URL = "https://pulse-api.simudyne.com"
16
86
  DEFAULT_TIMEOUT = 30
17
87
  RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
@@ -36,6 +106,8 @@ class PulseABM:
36
106
  from simudyne.resources.profile import ProfileResource
37
107
  from simudyne.resources.api_keys import ApiKeysResource
38
108
  from simudyne.resources.data import DataResource
109
+ from simudyne.resources.fix import FixResource
110
+ from simudyne.resources.fm import FmResource
39
111
  from simudyne.resources.simulation import SimulationResource
40
112
  from simudyne.resources.simulator_gym import SimulatorGymResource
41
113
  from simudyne.resources.validation import ValidationResource
@@ -43,6 +115,8 @@ class PulseABM:
43
115
  self.profile = ProfileResource(self)
44
116
  self.api_keys = ApiKeysResource(self)
45
117
  self.data = DataResource(self)
118
+ self.fix = FixResource(self)
119
+ self.fm = FmResource(self)
46
120
  self.simulation = SimulationResource(self)
47
121
  self.simulator_gym = SimulatorGymResource(self)
48
122
  self.validation = ValidationResource(self)
@@ -64,11 +138,7 @@ class PulseABM:
64
138
  time.sleep(delay)
65
139
  continue
66
140
 
67
- try:
68
- detail = response.json().get("detail", response.text)
69
- except ValueError:
70
- detail = response.text
71
- raise PulseAPIError(response.status_code, detail)
141
+ raise error_from_response(response)
72
142
 
73
143
  except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
74
144
  last_exception = e
@@ -85,6 +155,12 @@ class PulseABM:
85
155
  response = self._request_with_retries(method, url, **kwargs)
86
156
  return response.json()
87
157
 
158
+ def _request_text(self, method: str, endpoint: str, **kwargs):
159
+ """Like _request, for endpoints that answer text/plain not JSON."""
160
+ url = f"{self.base_url}{endpoint}"
161
+ response = self._request_with_retries(method, url, **kwargs)
162
+ return response.text
163
+
88
164
  def _request_csv(self, method, endpoint, **kwargs):
89
165
  PAGE_SIZE = 100000
90
166
  params = kwargs.get("params", {})