benchmaker 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchmaker/__init__.py +152 -0
- benchmaker/bundle.py +193 -0
- benchmaker/cli.py +382 -0
- benchmaker/collect.py +178 -0
- benchmaker/config.py +448 -0
- benchmaker/env.py +87 -0
- benchmaker/load.py +326 -0
- benchmaker/metrics.py +234 -0
- benchmaker/monitors.py +228 -0
- benchmaker/runner.py +275 -0
- benchmaker/trace.py +217 -0
- benchmaker/types.py +98 -0
- benchmaker/workloads/__init__.py +53 -0
- benchmaker/workloads/agent.py +308 -0
- benchmaker/workloads/base.py +79 -0
- benchmaker/workloads/datasets.py +156 -0
- benchmaker/workloads/eval.py +504 -0
- benchmaker/workloads/hf.py +382 -0
- benchmaker/workloads/http.py +77 -0
- benchmaker/workloads/llm.py +258 -0
- benchmaker/workloads/sandbox.py +470 -0
- benchmaker-0.1.0.dist-info/METADATA +214 -0
- benchmaker-0.1.0.dist-info/RECORD +26 -0
- benchmaker-0.1.0.dist-info/WHEEL +5 -0
- benchmaker-0.1.0.dist-info/entry_points.txt +2 -0
- benchmaker-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""HuggingFace `datasets` workload.
|
|
2
|
+
|
|
3
|
+
Yields per-request dicts shaped for downstream LLM workload-types — usually
|
|
4
|
+
`{"prompt": ..., "reference": ..., <extra_fields>: ...}`. Pair with
|
|
5
|
+
`OpenAIChatWorkloadType` (which promotes `prompt` → `messages`) and
|
|
6
|
+
`EvalWorkloadType` + `correctness_hook` (which reads `reference`).
|
|
7
|
+
|
|
8
|
+
The `datasets` library is an optional dependency. Install with:
|
|
9
|
+
|
|
10
|
+
pip install -e .[hf]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import random
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Callable, Iterable, Mapping, Optional, Union
|
|
18
|
+
|
|
19
|
+
from benchmaker.workloads.datasets import Workload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------------------- #
|
|
23
|
+
# Reference transforms (postprocessing on the raw reference field)
|
|
24
|
+
# --------------------------------------------------------------------------- #
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _gsm8k_answer(text: str) -> str:
|
|
28
|
+
"""GSM8K answers end in `#### <number>`; return that final number."""
|
|
29
|
+
if "####" in text:
|
|
30
|
+
return text.split("####")[-1].strip().replace(",", "")
|
|
31
|
+
m = re.search(r"-?\d+(?:\.\d+)?", text)
|
|
32
|
+
return m.group(0) if m else text.strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _strip(text: str) -> str:
|
|
36
|
+
return text.strip()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _first_line(text: str) -> str:
|
|
40
|
+
return text.split("\n", 1)[0].strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _lower_strip(text: str) -> str:
|
|
44
|
+
return text.strip().lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_TRANSFORMS: dict[str, Callable[[str], str]] = {
|
|
48
|
+
"identity": lambda s: s,
|
|
49
|
+
"strip": _strip,
|
|
50
|
+
"lower_strip": _lower_strip,
|
|
51
|
+
"first_line": _first_line,
|
|
52
|
+
"gsm8k": _gsm8k_answer,
|
|
53
|
+
"gsm8k_answer": _gsm8k_answer,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_transform(name: str) -> Callable[[str], str]:
|
|
58
|
+
try:
|
|
59
|
+
return _TRANSFORMS[name]
|
|
60
|
+
except KeyError:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"unknown reference_transform {name!r}; known: {sorted(_TRANSFORMS)}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --------------------------------------------------------------------------- #
|
|
67
|
+
# Dataset presets (path + subset + field names)
|
|
68
|
+
# --------------------------------------------------------------------------- #
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
PresetSpec = dict[str, Any]
|
|
72
|
+
|
|
73
|
+
_PRESETS: dict[str, PresetSpec] = {
|
|
74
|
+
"gsm8k": {
|
|
75
|
+
"path": "gsm8k",
|
|
76
|
+
"name": "main",
|
|
77
|
+
"split": "test",
|
|
78
|
+
"prompt_field": "question",
|
|
79
|
+
"reference_field": "answer",
|
|
80
|
+
"reference_transform": "gsm8k_answer",
|
|
81
|
+
},
|
|
82
|
+
"mmlu": {
|
|
83
|
+
# MMLU has many configs; default to "all". Override with `name=...`.
|
|
84
|
+
"path": "cais/mmlu",
|
|
85
|
+
"name": "all",
|
|
86
|
+
"split": "test",
|
|
87
|
+
# MMLU rows: question, choices (list[str]), answer (int idx). We build
|
|
88
|
+
# a multiple-choice prompt via a template and ship the letter answer.
|
|
89
|
+
"prompt_template": (
|
|
90
|
+
"{question}\n"
|
|
91
|
+
"A. {choice_a}\nB. {choice_b}\nC. {choice_c}\nD. {choice_d}\n"
|
|
92
|
+
"Answer with a single letter (A, B, C, or D)."
|
|
93
|
+
),
|
|
94
|
+
"prompt_template_fields": {
|
|
95
|
+
"question": "question",
|
|
96
|
+
"choice_a": ("choices", 0),
|
|
97
|
+
"choice_b": ("choices", 1),
|
|
98
|
+
"choice_c": ("choices", 2),
|
|
99
|
+
"choice_d": ("choices", 3),
|
|
100
|
+
},
|
|
101
|
+
"reference_field": "answer",
|
|
102
|
+
"reference_transform": "mmlu_letter",
|
|
103
|
+
},
|
|
104
|
+
"humaneval": {
|
|
105
|
+
"path": "openai_humaneval",
|
|
106
|
+
"split": "test",
|
|
107
|
+
"prompt_field": "prompt",
|
|
108
|
+
"reference_field": "canonical_solution",
|
|
109
|
+
"extra_fields": ("task_id", "test", "entry_point"),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Late-bound transforms that need argument bindings.
|
|
114
|
+
_TRANSFORMS["mmlu_letter"] = lambda v: ("ABCD"[int(v)] if isinstance(v, int)
|
|
115
|
+
or (isinstance(v, str) and v.isdigit())
|
|
116
|
+
else str(v).strip().upper()[:1])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_presets() -> list[str]:
|
|
120
|
+
return sorted(_PRESETS)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_preset(name: str) -> PresetSpec:
|
|
124
|
+
try:
|
|
125
|
+
return dict(_PRESETS[name])
|
|
126
|
+
except KeyError:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"unknown HF preset {name!r}; known: {list_presets()}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------------------- #
|
|
133
|
+
# The workload
|
|
134
|
+
# --------------------------------------------------------------------------- #
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class HFDatasetWorkload(Workload):
|
|
138
|
+
"""Load a HuggingFace dataset and yield `{prompt, reference, ...}` items.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
path: HuggingFace dataset id (`"gsm8k"`, `"cais/mmlu"`, ...) — required
|
|
142
|
+
unless `preset` is given.
|
|
143
|
+
name: dataset config / subset (`"main"` for gsm8k, etc.).
|
|
144
|
+
split: split name (default `"test"`).
|
|
145
|
+
preset: one of `list_presets()`. Fills in `path/name/split/field`
|
|
146
|
+
defaults; explicit kwargs override.
|
|
147
|
+
|
|
148
|
+
prompt_field: source row field whose value becomes `"prompt"`.
|
|
149
|
+
Mutually exclusive with `prompt_template`.
|
|
150
|
+
prompt_template: format string referencing fields from
|
|
151
|
+
`prompt_template_fields`. Use this when the prompt is composed of
|
|
152
|
+
several columns (MMLU, BBH, etc.).
|
|
153
|
+
prompt_template_fields: maps `{template_var: row_field}` (or
|
|
154
|
+
`(row_field, index)` to index into a list field). Required when
|
|
155
|
+
`prompt_template` is set.
|
|
156
|
+
|
|
157
|
+
reference_field: source row field whose value becomes `"reference"`.
|
|
158
|
+
Omit to skip producing a reference (e.g. for non-eval generation).
|
|
159
|
+
reference_transform: callable `(value) -> str` or a registered
|
|
160
|
+
transform name (`"gsm8k_answer"`, `"strip"`, ...).
|
|
161
|
+
|
|
162
|
+
extra_fields: row fields to carry through unchanged on each item
|
|
163
|
+
(handy for `EvalWorkloadType(extra_meta_keys=...)`).
|
|
164
|
+
|
|
165
|
+
max_items: stop after this many items.
|
|
166
|
+
shuffle: permute once at construction (non-streaming only).
|
|
167
|
+
seed: RNG seed for shuffle.
|
|
168
|
+
streaming: use `datasets`'s streaming mode (no shuffle, no len()).
|
|
169
|
+
loop: cycle the dataset when exhausted (non-streaming only).
|
|
170
|
+
cache_dir / token / trust_remote_code: forwarded to `load_dataset`.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
name: str = "hf"
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
path: Optional[str] = None,
|
|
178
|
+
*,
|
|
179
|
+
name: Optional[str] = None,
|
|
180
|
+
split: str = "test",
|
|
181
|
+
preset: Optional[str] = None,
|
|
182
|
+
|
|
183
|
+
prompt_field: Optional[str] = "prompt",
|
|
184
|
+
prompt_template: Optional[str] = None,
|
|
185
|
+
prompt_template_fields: Optional[Mapping[str, Any]] = None,
|
|
186
|
+
|
|
187
|
+
reference_field: Optional[str] = "reference",
|
|
188
|
+
reference_transform: Optional[Union[str, Callable[[Any], str]]] = None,
|
|
189
|
+
|
|
190
|
+
extra_fields: Iterable[str] = (),
|
|
191
|
+
|
|
192
|
+
max_items: Optional[int] = None,
|
|
193
|
+
shuffle: bool = False,
|
|
194
|
+
seed: Optional[int] = None,
|
|
195
|
+
streaming: bool = False,
|
|
196
|
+
loop: bool = False,
|
|
197
|
+
|
|
198
|
+
cache_dir: Optional[str] = None,
|
|
199
|
+
token: Optional[str] = None,
|
|
200
|
+
trust_remote_code: bool = False,
|
|
201
|
+
|
|
202
|
+
workload_name: Optional[str] = None,
|
|
203
|
+
):
|
|
204
|
+
spec: PresetSpec = {}
|
|
205
|
+
if preset:
|
|
206
|
+
spec = get_preset(preset)
|
|
207
|
+
|
|
208
|
+
path = path or spec.get("path")
|
|
209
|
+
if not path:
|
|
210
|
+
raise ValueError("HFDatasetWorkload requires `path` (or a `preset`)")
|
|
211
|
+
|
|
212
|
+
name = name if name is not None else spec.get("name")
|
|
213
|
+
# `split` is keyword-only with a default ("test"). Only fall back to
|
|
214
|
+
# the preset's value if the caller didn't explicitly override.
|
|
215
|
+
if split == "test" and "split" in spec:
|
|
216
|
+
split = spec["split"]
|
|
217
|
+
|
|
218
|
+
if prompt_template is None and spec.get("prompt_template"):
|
|
219
|
+
prompt_template = spec["prompt_template"]
|
|
220
|
+
prompt_template_fields = (prompt_template_fields
|
|
221
|
+
or spec.get("prompt_template_fields"))
|
|
222
|
+
prompt_field = None
|
|
223
|
+
elif prompt_field == "prompt" and "prompt_field" in spec:
|
|
224
|
+
prompt_field = spec["prompt_field"]
|
|
225
|
+
|
|
226
|
+
if reference_field == "reference" and "reference_field" in spec:
|
|
227
|
+
reference_field = spec["reference_field"]
|
|
228
|
+
|
|
229
|
+
if reference_transform is None and "reference_transform" in spec:
|
|
230
|
+
reference_transform = spec["reference_transform"]
|
|
231
|
+
|
|
232
|
+
if isinstance(reference_transform, str):
|
|
233
|
+
reference_transform = get_transform(reference_transform)
|
|
234
|
+
|
|
235
|
+
if prompt_template and not prompt_template_fields:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"prompt_template requires prompt_template_fields to map "
|
|
238
|
+
"template vars onto row fields"
|
|
239
|
+
)
|
|
240
|
+
if prompt_template and prompt_field:
|
|
241
|
+
# Caller supplied both; template wins (prompt_field auto-cleared
|
|
242
|
+
# above when preset injected the template).
|
|
243
|
+
prompt_field = None
|
|
244
|
+
if not prompt_template and not prompt_field:
|
|
245
|
+
raise ValueError("Either prompt_field or prompt_template must be set")
|
|
246
|
+
|
|
247
|
+
extra_fields = tuple(extra_fields) or tuple(spec.get("extra_fields") or ())
|
|
248
|
+
|
|
249
|
+
self._path = path
|
|
250
|
+
self._config_name = name
|
|
251
|
+
self._split = split
|
|
252
|
+
self._prompt_field = prompt_field
|
|
253
|
+
self._prompt_template = prompt_template
|
|
254
|
+
self._prompt_template_fields = dict(prompt_template_fields or {})
|
|
255
|
+
self._reference_field = reference_field
|
|
256
|
+
self._reference_transform = reference_transform
|
|
257
|
+
self._extra_fields = tuple(extra_fields)
|
|
258
|
+
self._max = max_items
|
|
259
|
+
self._loop = loop
|
|
260
|
+
self._streaming = streaming
|
|
261
|
+
self._cache_dir = cache_dir
|
|
262
|
+
self._token = token
|
|
263
|
+
self._trust_remote_code = trust_remote_code
|
|
264
|
+
|
|
265
|
+
if workload_name:
|
|
266
|
+
self.name = workload_name
|
|
267
|
+
else:
|
|
268
|
+
tag = path
|
|
269
|
+
if name:
|
|
270
|
+
tag = f"{tag}:{name}"
|
|
271
|
+
self.name = f"hf:{tag}/{split}"
|
|
272
|
+
|
|
273
|
+
self._iter: Any = None
|
|
274
|
+
self._n = 0
|
|
275
|
+
self._shuffle = shuffle
|
|
276
|
+
self._seed = seed
|
|
277
|
+
|
|
278
|
+
# ---- HF wiring ---- #
|
|
279
|
+
|
|
280
|
+
def _load(self) -> Any:
|
|
281
|
+
try:
|
|
282
|
+
from datasets import load_dataset
|
|
283
|
+
except ImportError as e:
|
|
284
|
+
raise ImportError(
|
|
285
|
+
"HFDatasetWorkload requires the `datasets` package. "
|
|
286
|
+
"Install with `pip install datasets` or `pip install -e .[hf]`."
|
|
287
|
+
) from e
|
|
288
|
+
|
|
289
|
+
kwargs: dict[str, Any] = {"split": self._split}
|
|
290
|
+
if self._config_name is not None:
|
|
291
|
+
kwargs["name"] = self._config_name
|
|
292
|
+
if self._cache_dir is not None:
|
|
293
|
+
kwargs["cache_dir"] = self._cache_dir
|
|
294
|
+
if self._token is not None:
|
|
295
|
+
kwargs["token"] = self._token
|
|
296
|
+
if self._trust_remote_code:
|
|
297
|
+
kwargs["trust_remote_code"] = True
|
|
298
|
+
if self._streaming:
|
|
299
|
+
kwargs["streaming"] = True
|
|
300
|
+
return load_dataset(self._path, **kwargs)
|
|
301
|
+
|
|
302
|
+
def _ensure_iter(self) -> None:
|
|
303
|
+
if self._iter is not None:
|
|
304
|
+
return
|
|
305
|
+
ds = self._load()
|
|
306
|
+
if self._streaming:
|
|
307
|
+
self._iter = iter(ds)
|
|
308
|
+
self._source = None
|
|
309
|
+
else:
|
|
310
|
+
indices = list(range(len(ds)))
|
|
311
|
+
if self._shuffle:
|
|
312
|
+
random.Random(self._seed).shuffle(indices)
|
|
313
|
+
self._source = ds
|
|
314
|
+
self._indices = indices
|
|
315
|
+
self._cursor = 0
|
|
316
|
+
self._iter = self._yield_indexed()
|
|
317
|
+
|
|
318
|
+
def _yield_indexed(self):
|
|
319
|
+
while True:
|
|
320
|
+
if self._cursor >= len(self._indices):
|
|
321
|
+
if not self._loop:
|
|
322
|
+
return
|
|
323
|
+
self._cursor = 0
|
|
324
|
+
idx = self._indices[self._cursor]
|
|
325
|
+
self._cursor += 1
|
|
326
|
+
yield self._source[idx]
|
|
327
|
+
|
|
328
|
+
# ---- item shaping ---- #
|
|
329
|
+
|
|
330
|
+
def _shape(self, row: Mapping[str, Any]) -> dict[str, Any]:
|
|
331
|
+
item: dict[str, Any] = {}
|
|
332
|
+
if self._prompt_template:
|
|
333
|
+
ctx: dict[str, Any] = {}
|
|
334
|
+
for var, src in self._prompt_template_fields.items():
|
|
335
|
+
ctx[var] = _lookup(row, src)
|
|
336
|
+
item["prompt"] = self._prompt_template.format(**ctx)
|
|
337
|
+
elif self._prompt_field:
|
|
338
|
+
item["prompt"] = row[self._prompt_field]
|
|
339
|
+
|
|
340
|
+
if self._reference_field and self._reference_field in row:
|
|
341
|
+
value = row[self._reference_field]
|
|
342
|
+
if self._reference_transform is not None:
|
|
343
|
+
value = self._reference_transform(value)
|
|
344
|
+
item["reference"] = value
|
|
345
|
+
|
|
346
|
+
for f in self._extra_fields:
|
|
347
|
+
if f in row:
|
|
348
|
+
item[f] = row[f]
|
|
349
|
+
return item
|
|
350
|
+
|
|
351
|
+
# ---- Workload protocol ---- #
|
|
352
|
+
|
|
353
|
+
async def next_item(self) -> Any:
|
|
354
|
+
if self._max is not None and self._n >= self._max:
|
|
355
|
+
raise StopAsyncIteration
|
|
356
|
+
self._ensure_iter()
|
|
357
|
+
try:
|
|
358
|
+
row = next(self._iter)
|
|
359
|
+
except StopIteration:
|
|
360
|
+
raise StopAsyncIteration
|
|
361
|
+
self._n += 1
|
|
362
|
+
return self._shape(row)
|
|
363
|
+
|
|
364
|
+
async def aclose(self) -> None:
|
|
365
|
+
self._iter = None
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _lookup(row: Mapping[str, Any], src: Any) -> Any:
|
|
370
|
+
"""Resolve a source spec against a row.
|
|
371
|
+
|
|
372
|
+
`src` is either a field name (`"question"`) or a tuple
|
|
373
|
+
`(field_name, index, ...)` for indexing into list/dict fields.
|
|
374
|
+
"""
|
|
375
|
+
if isinstance(src, str):
|
|
376
|
+
return row[src]
|
|
377
|
+
if isinstance(src, tuple) and src:
|
|
378
|
+
value = row[src[0]]
|
|
379
|
+
for key in src[1:]:
|
|
380
|
+
value = value[key]
|
|
381
|
+
return value
|
|
382
|
+
raise TypeError(f"bad template field spec {src!r}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Generic HTTP workload-type.
|
|
2
|
+
|
|
3
|
+
Interprets workload items flexibly:
|
|
4
|
+
|
|
5
|
+
* `None` → fire the base request unchanged.
|
|
6
|
+
* `bytes` → use as raw body.
|
|
7
|
+
* `str` → use as raw body (utf-8 encoded).
|
|
8
|
+
* `dict` → use as JSON body, UNLESS keys overlap with Request fields
|
|
9
|
+
(`body`/`json`/`params`/`headers`/`url`/`method`/`meta`),
|
|
10
|
+
in which case those fields are applied directly. This
|
|
11
|
+
lets a dataset fully customize the request when needed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from benchmaker.types import Request
|
|
19
|
+
from benchmaker.workloads.base import WorkloadType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_REQUEST_KEYS = {"body", "json", "params", "headers", "url", "method", "meta", "timeout_s"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HttpWorkloadType(WorkloadType):
|
|
26
|
+
name = "http"
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
url: str = "",
|
|
31
|
+
method: str = "GET",
|
|
32
|
+
headers: Optional[dict[str, str]] = None,
|
|
33
|
+
params: Optional[dict[str, Any]] = None,
|
|
34
|
+
timeout_s: Optional[float] = None,
|
|
35
|
+
name: str = "http",
|
|
36
|
+
):
|
|
37
|
+
self.name = name
|
|
38
|
+
self._url = url
|
|
39
|
+
self._method = method
|
|
40
|
+
self._headers = headers or {}
|
|
41
|
+
self._params = params or {}
|
|
42
|
+
self._timeout_s = timeout_s
|
|
43
|
+
|
|
44
|
+
async def make_request(self, item: Any) -> Request:
|
|
45
|
+
req = Request(
|
|
46
|
+
method=self._method,
|
|
47
|
+
url=self._url,
|
|
48
|
+
headers=dict(self._headers),
|
|
49
|
+
params=dict(self._params),
|
|
50
|
+
timeout_s=self._timeout_s,
|
|
51
|
+
)
|
|
52
|
+
if item is None:
|
|
53
|
+
return req
|
|
54
|
+
if isinstance(item, (bytes, bytearray)):
|
|
55
|
+
req.body = bytes(item)
|
|
56
|
+
return req
|
|
57
|
+
if isinstance(item, str):
|
|
58
|
+
req.body = item.encode("utf-8")
|
|
59
|
+
return req
|
|
60
|
+
if isinstance(item, dict):
|
|
61
|
+
if _REQUEST_KEYS & item.keys():
|
|
62
|
+
# Treat as a Request override.
|
|
63
|
+
for k, v in item.items():
|
|
64
|
+
if k == "headers":
|
|
65
|
+
req.headers.update(v)
|
|
66
|
+
elif k == "params":
|
|
67
|
+
req.params.update(v)
|
|
68
|
+
elif k == "meta":
|
|
69
|
+
req.meta.update(v)
|
|
70
|
+
elif k in _REQUEST_KEYS:
|
|
71
|
+
setattr(req, k, v)
|
|
72
|
+
# ignore unknown keys silently
|
|
73
|
+
return req
|
|
74
|
+
# Otherwise treat the whole dict as the JSON body.
|
|
75
|
+
req.json = item
|
|
76
|
+
return req
|
|
77
|
+
raise TypeError(f"HttpWorkloadType cannot interpret item of type {type(item).__name__}")
|