simudyne-pulse 0.7.0.dev15__tar.gz → 0.7.0.dev16__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.7.0.dev15/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev16}/PKG-INFO +28 -1
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/README.md +27 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/pyproject.toml +1 -1
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/validation.py +114 -53
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16/src/simudyne_pulse.egg-info}/PKG-INFO +28 -1
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/tests/test_validation.py +55 -2
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/client.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/fm.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/simulation.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/tests/test_new_resources.py +0 -0
- {simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/tests/test_simulator_gym.py +0 -0
{simudyne_pulse-0.7.0.dev15/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev16}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simudyne-pulse
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev16
|
|
4
4
|
Summary: Python SDK for the Simudyne Pulse synthetic market data API
|
|
5
5
|
Author-email: Simudyne <support@simudyne.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,6 +85,33 @@ population. `sim_files` groups the same way.
|
|
|
85
85
|
A plain list is one unnamed population and keeps the flat shape exactly as
|
|
86
86
|
before. The historical day is measured once however many groups there are.
|
|
87
87
|
|
|
88
|
+
### Your own runs beside platform runs
|
|
89
|
+
|
|
90
|
+
`sim_ids` and `sim_files` can be given together — your own model against
|
|
91
|
+
platform simulations, on one set of figures, 25 runs in total across the two:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
job = client.validation.run(
|
|
95
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
96
|
+
sim_files={"fm": ["my_run.parquet"]},
|
|
97
|
+
sim_ids={"abm": abm_ids},
|
|
98
|
+
plots=["statistical.radar", "stylised_facts.overall"],
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With both present each source is a population of its own, so a bare list gets
|
|
103
|
+
a name rather than merging into whatever else is there:
|
|
104
|
+
|
|
105
|
+
| passed | populations |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| files mapping + ids list | its names, plus `platform` |
|
|
108
|
+
| files list + ids mapping | `uploaded`, plus its names |
|
|
109
|
+
| both lists | `uploaded` and `platform` |
|
|
110
|
+
| both mappings | their own names |
|
|
111
|
+
| either one alone | exactly as before |
|
|
112
|
+
|
|
113
|
+
The uploads are measured first and the platform runs follow.
|
|
114
|
+
|
|
88
115
|
`plots` is the only plot switch: unset draws nothing, `True` draws every
|
|
89
116
|
figure the enabled areas can draw, and a list draws just those ids. An area
|
|
90
117
|
switched off draws nothing either way.
|
|
@@ -59,6 +59,33 @@ population. `sim_files` groups the same way.
|
|
|
59
59
|
A plain list is one unnamed population and keeps the flat shape exactly as
|
|
60
60
|
before. The historical day is measured once however many groups there are.
|
|
61
61
|
|
|
62
|
+
### Your own runs beside platform runs
|
|
63
|
+
|
|
64
|
+
`sim_ids` and `sim_files` can be given together — your own model against
|
|
65
|
+
platform simulations, on one set of figures, 25 runs in total across the two:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
job = client.validation.run(
|
|
69
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
70
|
+
sim_files={"fm": ["my_run.parquet"]},
|
|
71
|
+
sim_ids={"abm": abm_ids},
|
|
72
|
+
plots=["statistical.radar", "stylised_facts.overall"],
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
With both present each source is a population of its own, so a bare list gets
|
|
77
|
+
a name rather than merging into whatever else is there:
|
|
78
|
+
|
|
79
|
+
| passed | populations |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| files mapping + ids list | its names, plus `platform` |
|
|
82
|
+
| files list + ids mapping | `uploaded`, plus its names |
|
|
83
|
+
| both lists | `uploaded` and `platform` |
|
|
84
|
+
| both mappings | their own names |
|
|
85
|
+
| either one alone | exactly as before |
|
|
86
|
+
|
|
87
|
+
The uploads are measured first and the platform runs follow.
|
|
88
|
+
|
|
62
89
|
`plots` is the only plot switch: unset draws nothing, `True` draws every
|
|
63
90
|
figure the enabled areas can draw, and a list draws just those ids. An area
|
|
64
91
|
switched off draws nothing either way.
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/validation.py
RENAMED
|
@@ -108,6 +108,62 @@ def _frame_to_parquet(entry, index: int):
|
|
|
108
108
|
return path.name, path.read_bytes()
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def _count(sims) -> int:
|
|
112
|
+
"""How many runs this argument names, mapping or list."""
|
|
113
|
+
if sims is None:
|
|
114
|
+
return 0
|
|
115
|
+
if isinstance(sims, dict):
|
|
116
|
+
return sum(len(list(v)) for v in sims.values())
|
|
117
|
+
return len(sims)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_groups(sims, name: str) -> None:
|
|
121
|
+
"""Every group's value is a list of runs, and a bare string is not one.
|
|
122
|
+
|
|
123
|
+
``{"fm": "one.parquet"}`` is the natural thing to write for a group of
|
|
124
|
+
one. A string is iterable, so it used to count the path's characters as
|
|
125
|
+
runs and fail on the total — a number the caller never wrote.
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(sims, dict):
|
|
128
|
+
return
|
|
129
|
+
for group, runs in sims.items():
|
|
130
|
+
if isinstance(runs, (str, bytes)):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"{name}[{group!r}] must be a list of runs, not a "
|
|
133
|
+
f"single {type(runs).__name__} — write [{runs!r}]"
|
|
134
|
+
)
|
|
135
|
+
if not isinstance(runs, Iterable):
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"{name}[{group!r}] must be a list of runs, not "
|
|
138
|
+
f"{type(runs).__name__}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _flatten(sims, *, offset: int, unnamed):
|
|
143
|
+
"""``(flat runs, {name: [indices]} | None)``, indices starting at offset.
|
|
144
|
+
|
|
145
|
+
A job may draw from two sources at once: the uploaded files take the first
|
|
146
|
+
slots and the platform runs follow, so the second source's indices start
|
|
147
|
+
past the first. ``unnamed`` names a bare list so it can be a population
|
|
148
|
+
beside a named one; None leaves it ungrouped, as a single-source job has
|
|
149
|
+
always been.
|
|
150
|
+
"""
|
|
151
|
+
if isinstance(sims, dict):
|
|
152
|
+
flat, groups, at = [], {}, offset
|
|
153
|
+
for name, runs in sims.items():
|
|
154
|
+
runs = list(runs)
|
|
155
|
+
if not runs:
|
|
156
|
+
raise ValueError(f"Group {name!r} has no simulations")
|
|
157
|
+
groups[name] = list(range(at, at + len(runs)))
|
|
158
|
+
at += len(runs)
|
|
159
|
+
flat.extend(runs)
|
|
160
|
+
return flat, groups
|
|
161
|
+
flat = list(sims)
|
|
162
|
+
if unnamed is None:
|
|
163
|
+
return flat, None
|
|
164
|
+
return flat, {unnamed: list(range(offset, offset + len(flat)))}
|
|
165
|
+
|
|
166
|
+
|
|
111
167
|
def _build_config(n_levels, **flags) -> dict:
|
|
112
168
|
"""The validation config as the API expects it.
|
|
113
169
|
|
|
@@ -234,11 +290,29 @@ class ValidationResource:
|
|
|
234
290
|
the named populations: distances, distributions, verdicts and
|
|
235
291
|
FID/MIND scores come back keyed by name, and the summary figures
|
|
236
292
|
draw one series per name instead of averaging them together.
|
|
237
|
-
Exactly one of ``sim_ids`` or ``sim_files`` is required.
|
|
238
293
|
sim_files : list or dict, optional
|
|
239
|
-
Your own simulated runs
|
|
240
|
-
|
|
241
|
-
|
|
294
|
+
Your own simulated runs: paths, ``(filename, bytes)`` pairs, or
|
|
295
|
+
polars / pandas DataFrames in pulse format. Grouped the same way
|
|
296
|
+
as ``sim_ids``. The historical side is fetched for you.
|
|
297
|
+
|
|
298
|
+
At least one of ``sim_ids`` and ``sim_files`` is required, and
|
|
299
|
+
both together is allowed — your own model against platform runs,
|
|
300
|
+
on one set of figures. 25 runs in total across the two. With both
|
|
301
|
+
present each source is a population of its own, so a bare list
|
|
302
|
+
gets a name rather than merging into whatever else is there:
|
|
303
|
+
|
|
304
|
+
=========================== ==============================
|
|
305
|
+
passed populations
|
|
306
|
+
=========================== ==============================
|
|
307
|
+
files mapping + ids list its names, plus ``platform``
|
|
308
|
+
files list + ids mapping ``uploaded``, plus its names
|
|
309
|
+
both lists ``uploaded`` and ``platform``
|
|
310
|
+
both mappings their own names
|
|
311
|
+
either one alone exactly as before
|
|
312
|
+
=========================== ==============================
|
|
313
|
+
|
|
314
|
+
Every population is pooled and averaged, and drawn as its own
|
|
315
|
+
radar polygon, verdict-grid column and correlation heatmap.
|
|
242
316
|
statistical, stylised_facts, impact, volume_correlation, fid, mind : bool, optional
|
|
243
317
|
Whether to run each area. ``None`` uses your tier's default.
|
|
244
318
|
lob : bool, optional
|
|
@@ -338,36 +412,19 @@ class ValidationResource:
|
|
|
338
412
|
... sample_period="1s",
|
|
339
413
|
... )
|
|
340
414
|
"""
|
|
341
|
-
if
|
|
342
|
-
raise ValueError("Pass
|
|
415
|
+
if sim_ids is None and sim_files is None:
|
|
416
|
+
raise ValueError("Pass sim_ids, sim_files, or both")
|
|
343
417
|
if sim_ids is not None and not sim_ids:
|
|
344
418
|
raise ValueError("sim_ids must not be empty")
|
|
345
419
|
if sim_files is not None and not sim_files:
|
|
346
420
|
raise ValueError("sim_files must not be empty")
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if isinstance(runs, (str, bytes)):
|
|
355
|
-
raise ValueError(
|
|
356
|
-
f"{name}[{group!r}] must be a list of runs, not a "
|
|
357
|
-
f"single {type(runs).__name__} — write [{runs!r}]"
|
|
358
|
-
)
|
|
359
|
-
if not isinstance(runs, Iterable):
|
|
360
|
-
raise ValueError(
|
|
361
|
-
f"{name}[{group!r}] must be a list of runs, not "
|
|
362
|
-
f"{type(runs).__name__}"
|
|
363
|
-
)
|
|
364
|
-
count = (
|
|
365
|
-
sum(len(list(v)) for v in given.values())
|
|
366
|
-
if isinstance(given, dict)
|
|
367
|
-
else len(given)
|
|
368
|
-
)
|
|
369
|
-
if count > MAX_SIM_FILES:
|
|
370
|
-
raise ValueError(f"Maximum {MAX_SIM_FILES} simulations per validation job")
|
|
421
|
+
_check_groups(sim_files, "sim_files")
|
|
422
|
+
_check_groups(sim_ids, "sim_ids")
|
|
423
|
+
if _count(sim_files) + _count(sim_ids) > MAX_SIM_FILES:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
f"Maximum {MAX_SIM_FILES} simulations per validation job, "
|
|
426
|
+
"sim_files and sim_ids together"
|
|
427
|
+
)
|
|
371
428
|
if n_levels < 1:
|
|
372
429
|
raise ValueError("n_levels must be at least 1")
|
|
373
430
|
|
|
@@ -385,7 +442,10 @@ class ValidationResource:
|
|
|
385
442
|
plots=plots,
|
|
386
443
|
)
|
|
387
444
|
|
|
388
|
-
|
|
445
|
+
# Platform runs alone go as JSON; anything with files of your own
|
|
446
|
+
# goes multipart, because bytes cannot travel in the JSON body. The
|
|
447
|
+
# upload endpoint takes sim_ids too, so a mixed job is one request.
|
|
448
|
+
if sim_files is None:
|
|
389
449
|
submitted = self._client._request(
|
|
390
450
|
"POST",
|
|
391
451
|
RUN_PATH,
|
|
@@ -403,37 +463,38 @@ class ValidationResource:
|
|
|
403
463
|
},
|
|
404
464
|
)
|
|
405
465
|
else:
|
|
466
|
+
# The uploads take the first slots of the combined run list and
|
|
467
|
+
# the platform runs follow. Only the uploads' own grouping is sent:
|
|
468
|
+
# the server offsets the platform runs past the files and names a
|
|
469
|
+
# bare list on either side, so the offset has one implementation
|
|
470
|
+
# rather than two that can disagree.
|
|
471
|
+
flat, groups = _flatten(sim_files, offset=0, unnamed=None)
|
|
472
|
+
|
|
406
473
|
# Three-tuple parts: the content type matters to the server's
|
|
407
474
|
# multipart parser, so it is sent explicitly.
|
|
408
|
-
# Grouped uploads travel flat with the grouping beside them, the
|
|
409
|
-
# same way sim_ids do — multipart has no nesting.
|
|
410
|
-
if isinstance(sim_files, dict):
|
|
411
|
-
flat, groups, offset = [], {}, 0
|
|
412
|
-
for name, runs in sim_files.items():
|
|
413
|
-
runs = list(runs)
|
|
414
|
-
groups[name] = list(range(offset, offset + len(runs)))
|
|
415
|
-
offset += len(runs)
|
|
416
|
-
flat.extend(runs)
|
|
417
|
-
else:
|
|
418
|
-
flat, groups = list(sim_files), None
|
|
419
|
-
|
|
420
475
|
files = [
|
|
421
476
|
("sim_files", (*_frame_to_parquet(entry, i), "application/octet-stream"))
|
|
422
477
|
for i, entry in enumerate(flat)
|
|
423
478
|
]
|
|
479
|
+
# Multipart has no nesting, so the grouping rides in the config
|
|
480
|
+
# and the ids in a JSON string field.
|
|
424
481
|
if groups:
|
|
425
482
|
config = {**config, "sim_groups": groups}
|
|
483
|
+
data = {
|
|
484
|
+
"symbol": symbol,
|
|
485
|
+
"date": date,
|
|
486
|
+
"provider": provider,
|
|
487
|
+
"exchange": exchange,
|
|
488
|
+
"config": json.dumps(config),
|
|
489
|
+
}
|
|
490
|
+
if sim_ids is not None:
|
|
491
|
+
data["sim_ids"] = json.dumps(
|
|
492
|
+
{k: list(v) for k, v in sim_ids.items()}
|
|
493
|
+
if isinstance(sim_ids, dict)
|
|
494
|
+
else list(sim_ids)
|
|
495
|
+
)
|
|
426
496
|
submitted = self._client._request(
|
|
427
|
-
"POST",
|
|
428
|
-
UPLOAD_PATH,
|
|
429
|
-
files=files,
|
|
430
|
-
data={
|
|
431
|
-
"symbol": symbol,
|
|
432
|
-
"date": date,
|
|
433
|
-
"provider": provider,
|
|
434
|
-
"exchange": exchange,
|
|
435
|
-
"config": json.dumps(config),
|
|
436
|
-
},
|
|
497
|
+
"POST", UPLOAD_PATH, files=files, data=data
|
|
437
498
|
)
|
|
438
499
|
|
|
439
500
|
if not (submitted or {}).get("job_id"):
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16/src/simudyne_pulse.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simudyne-pulse
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev16
|
|
4
4
|
Summary: Python SDK for the Simudyne Pulse synthetic market data API
|
|
5
5
|
Author-email: Simudyne <support@simudyne.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,6 +85,33 @@ population. `sim_files` groups the same way.
|
|
|
85
85
|
A plain list is one unnamed population and keeps the flat shape exactly as
|
|
86
86
|
before. The historical day is measured once however many groups there are.
|
|
87
87
|
|
|
88
|
+
### Your own runs beside platform runs
|
|
89
|
+
|
|
90
|
+
`sim_ids` and `sim_files` can be given together — your own model against
|
|
91
|
+
platform simulations, on one set of figures, 25 runs in total across the two:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
job = client.validation.run(
|
|
95
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
96
|
+
sim_files={"fm": ["my_run.parquet"]},
|
|
97
|
+
sim_ids={"abm": abm_ids},
|
|
98
|
+
plots=["statistical.radar", "stylised_facts.overall"],
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With both present each source is a population of its own, so a bare list gets
|
|
103
|
+
a name rather than merging into whatever else is there:
|
|
104
|
+
|
|
105
|
+
| passed | populations |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| files mapping + ids list | its names, plus `platform` |
|
|
108
|
+
| files list + ids mapping | `uploaded`, plus its names |
|
|
109
|
+
| both lists | `uploaded` and `platform` |
|
|
110
|
+
| both mappings | their own names |
|
|
111
|
+
| either one alone | exactly as before |
|
|
112
|
+
|
|
113
|
+
The uploads are measured first and the platform runs follow.
|
|
114
|
+
|
|
88
115
|
`plots` is the only plot switch: unset draws nothing, `True` draws every
|
|
89
116
|
figure the enabled areas can draw, and a list draws just those ids. An area
|
|
90
117
|
switched off draws nothing either way.
|
|
@@ -79,8 +79,7 @@ class TestWhatToValidate:
|
|
|
79
79
|
@pytest.mark.parametrize(
|
|
80
80
|
"kwargs,match",
|
|
81
81
|
[
|
|
82
|
-
({}, "
|
|
83
|
-
({"sim_ids": SIM_IDS, "sim_files": [("a", b"x")]}, "exactly one"),
|
|
82
|
+
({}, "Pass sim_ids, sim_files, or both"),
|
|
84
83
|
({"sim_ids": []}, "must not be empty"),
|
|
85
84
|
({"sim_ids": SIM_IDS * (MAX_SIM_FILES + 1)}, "Maximum 25"),
|
|
86
85
|
({"sim_ids": SIM_IDS, "n_levels": 0}, "at least 1"),
|
|
@@ -353,3 +352,57 @@ class TestTickSizeIsNotAsked:
|
|
|
353
352
|
client = FakeClient([{"job_id": "v1"}])
|
|
354
353
|
ValidationResource(client).run(sim_files=[("a.parquet", b"x")], **IDENTITY)
|
|
355
354
|
assert "ticksize" not in client.calls[0][2]["data"]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestBothSourcesAtOnce:
|
|
358
|
+
"""Your own parquet and platform runs in one job.
|
|
359
|
+
|
|
360
|
+
Anything with files goes multipart, because bytes cannot travel in a JSON
|
|
361
|
+
body — so a mixed job is an upload that also names sim_ids. Only the
|
|
362
|
+
uploads' own grouping is sent: the server offsets the platform runs past
|
|
363
|
+
the files, so the offset has one implementation rather than two.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _submit(**kwargs):
|
|
368
|
+
client = FakeClient([{"job_id": "v1"}])
|
|
369
|
+
ValidationResource(client).run(**{**IDENTITY, **kwargs})
|
|
370
|
+
call = client.calls[0][2]
|
|
371
|
+
return call, json.loads(call["data"]["config"])
|
|
372
|
+
|
|
373
|
+
def test_a_mixed_job_goes_to_the_upload_endpoint(self):
|
|
374
|
+
call, _ = self._submit(
|
|
375
|
+
sim_files=[("a.parquet", b"x")], sim_ids=["sim_a"]
|
|
376
|
+
)
|
|
377
|
+
assert "files" in call and call["data"]["sim_ids"] == json.dumps(["sim_a"])
|
|
378
|
+
|
|
379
|
+
def test_only_the_uploads_grouping_is_sent(self):
|
|
380
|
+
"""The server owns the offset; sending both would double-count it."""
|
|
381
|
+
_, config = self._submit(
|
|
382
|
+
sim_files={"fm": [("a.parquet", b"x")]}, sim_ids={"abm": ["s"]}
|
|
383
|
+
)
|
|
384
|
+
assert config["sim_groups"] == {"fm": [0]}
|
|
385
|
+
|
|
386
|
+
def test_a_mapping_of_ids_travels_as_json(self):
|
|
387
|
+
call, _ = self._submit(
|
|
388
|
+
sim_files=[("a.parquet", b"x")], sim_ids={"abm": ["s1", "s2"]}
|
|
389
|
+
)
|
|
390
|
+
assert json.loads(call["data"]["sim_ids"]) == {"abm": ["s1", "s2"]}
|
|
391
|
+
|
|
392
|
+
def test_ids_alone_still_go_as_json(self):
|
|
393
|
+
client = FakeClient([{"job_id": "v1"}])
|
|
394
|
+
ValidationResource(client).run(sim_ids=SIM_IDS, **IDENTITY)
|
|
395
|
+
assert "json" in client.calls[0][2]
|
|
396
|
+
assert "sim_ids" not in client.calls[0][2].get("data", {})
|
|
397
|
+
|
|
398
|
+
def test_neither_is_refused(self):
|
|
399
|
+
with pytest.raises(ValueError, match="Pass sim_ids, sim_files, or both"):
|
|
400
|
+
ValidationResource(FakeClient([])).run(**IDENTITY)
|
|
401
|
+
|
|
402
|
+
def test_the_cap_counts_both_sources(self):
|
|
403
|
+
with pytest.raises(ValueError, match="together"):
|
|
404
|
+
ValidationResource(FakeClient([])).run(
|
|
405
|
+
sim_files=[("a.parquet", b"x")] * 13,
|
|
406
|
+
sim_ids=["s"] * 13,
|
|
407
|
+
**IDENTITY,
|
|
408
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/__init__.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/api_keys.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/simulation.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne/resources/simulator_gym.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/requires.txt
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev15 → simudyne_pulse-0.7.0.dev16}/src/simudyne_pulse.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|