blicket 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.
- blicket/__init__.py +62 -0
- blicket/_validation.py +345 -0
- blicket/_viewer/_nuxt/1cOsiU4V.js +17 -0
- blicket/_viewer/_nuxt/B1Ajr_4I.js +1 -0
- blicket/_viewer/_nuxt/BDNMzG2s.js +1 -0
- blicket/_viewer/_nuxt/BEEqn56o.js +6 -0
- blicket/_viewer/_nuxt/BHzbO7xK.js +5 -0
- blicket/_viewer/_nuxt/BKlKgU3-.js +1 -0
- blicket/_viewer/_nuxt/BPPywRHg.js +1 -0
- blicket/_viewer/_nuxt/BXEw9c8W.js +1 -0
- blicket/_viewer/_nuxt/B_VLRQDZ.js +1 -0
- blicket/_viewer/_nuxt/Beje1szI.js +10 -0
- blicket/_viewer/_nuxt/CMbIX9Kl.js +1 -0
- blicket/_viewer/_nuxt/CWOhNT74.js +188 -0
- blicket/_viewer/_nuxt/CXM8Gw78.js +1 -0
- blicket/_viewer/_nuxt/D8k6inko.js +2 -0
- blicket/_viewer/_nuxt/DTS3p6Kw.js +1 -0
- blicket/_viewer/_nuxt/DZudwwRl.js +1 -0
- blicket/_viewer/_nuxt/D_1mcQyk.js +1 -0
- blicket/_viewer/_nuxt/HeAlZHPs.js +1 -0
- blicket/_viewer/_nuxt/KEV6aDTx.js +985 -0
- blicket/_viewer/_nuxt/M236AyiD.js +3 -0
- blicket/_viewer/_nuxt/MfVtmpT3.js +1 -0
- blicket/_viewer/_nuxt/UpNOlQ57.js +1 -0
- blicket/_viewer/_nuxt/UxTkzkgn.js +13 -0
- blicket/_viewer/_nuxt/YGQZaGss.js +1 -0
- blicket/_viewer/_nuxt/dzXin_hD.js +1 -0
- blicket/_viewer/_nuxt/entry.B8omGnxT.css +2 -0
- blicket/_viewer/_nuxt/error-404.z9L05_G3.css +1 -0
- blicket/_viewer/_nuxt/error-500.C5Y40PvL.css +1 -0
- blicket/_viewer/_nuxt/ooN_Dj2v.js +3 -0
- blicket/_viewer/_nuxt/wGM7sQ7_.js +1 -0
- blicket/_viewer/favicon.svg +1 -0
- blicket/_viewer/index.html +8 -0
- blicket/assembly.py +321 -0
- blicket/baseline.py +168 -0
- blicket/builder.py +271 -0
- blicket/check.py +39 -0
- blicket/cli.py +447 -0
- blicket/comparison.py +352 -0
- blicket/config.py +121 -0
- blicket/dataset.py +428 -0
- blicket/definitions.py +270 -0
- blicket/dev.py +203 -0
- blicket/discovery.py +269 -0
- blicket/errors.py +160 -0
- blicket/loader.py +139 -0
- blicket/manifest.py +183 -0
- blicket/model.py +295 -0
- blicket/plots/__init__.py +49 -0
- blicket/plots/_base.py +246 -0
- blicket/plots/_diff.py +209 -0
- blicket/plots/bar.py +42 -0
- blicket/plots/line.py +37 -0
- blicket/plots/sankey.py +44 -0
- blicket/plots/scatter.py +47 -0
- blicket/plots/stacked_area.py +23 -0
- blicket/py.typed +0 -0
- blicket/query.py +428 -0
- blicket/report.py +530 -0
- blicket/schema.py +129 -0
- blicket/serve.py +494 -0
- blicket/store.py +152 -0
- blicket/tables.py +483 -0
- blicket/tracebacks.py +61 -0
- blicket/viewer.py +95 -0
- blicket-0.1.0.dist-info/METADATA +672 -0
- blicket-0.1.0.dist-info/RECORD +71 -0
- blicket-0.1.0.dist-info/WHEEL +4 -0
- blicket-0.1.0.dist-info/entry_points.txt +3 -0
- blicket-0.1.0.dist-info/licenses/LICENSE +21 -0
blicket/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""blicket: Storybook for scientific model outputs.
|
|
2
|
+
|
|
3
|
+
Blick modules use this package as ``import blicket as bk``::
|
|
4
|
+
|
|
5
|
+
bk.group("flows", title="Material flows", tags=["mfa"])
|
|
6
|
+
|
|
7
|
+
@bk.blick(title="Steel flows", plot=bk.plots.stacked_area(x="year", y="value", color="process"))
|
|
8
|
+
def steel_by_process(run): ...
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from blicket import plots
|
|
12
|
+
from blicket.builder import Build, NothingToBuild, build
|
|
13
|
+
from blicket.comparison import Comparison
|
|
14
|
+
from blicket.config import Config, find_project_root, load_config
|
|
15
|
+
from blicket.dataset import Dataset, Frames, Quantity
|
|
16
|
+
from blicket.definitions import blick, group, register, slug
|
|
17
|
+
from blicket.discovery import discover
|
|
18
|
+
from blicket.errors import BlicketError, ConfigError, DefinitionError, LoaderError, Problem, Skip
|
|
19
|
+
from blicket.loader import RunSelection, fingerprint
|
|
20
|
+
from blicket.manifest import Manifest, RunInfo
|
|
21
|
+
from blicket.model import Blick, BlickEntry, Group, Registry
|
|
22
|
+
from blicket.plots import Channel, PlotSpec, PlotType
|
|
23
|
+
from blicket.schema import Change, Control, Dimension
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Blick",
|
|
27
|
+
"BlickEntry",
|
|
28
|
+
"BlicketError",
|
|
29
|
+
"Build",
|
|
30
|
+
"Change",
|
|
31
|
+
"Channel",
|
|
32
|
+
"Comparison",
|
|
33
|
+
"Config",
|
|
34
|
+
"ConfigError",
|
|
35
|
+
"Control",
|
|
36
|
+
"Dataset",
|
|
37
|
+
"DefinitionError",
|
|
38
|
+
"Dimension",
|
|
39
|
+
"Frames",
|
|
40
|
+
"Group",
|
|
41
|
+
"LoaderError",
|
|
42
|
+
"Manifest",
|
|
43
|
+
"NothingToBuild",
|
|
44
|
+
"PlotSpec",
|
|
45
|
+
"PlotType",
|
|
46
|
+
"Problem",
|
|
47
|
+
"Quantity",
|
|
48
|
+
"Registry",
|
|
49
|
+
"RunInfo",
|
|
50
|
+
"RunSelection",
|
|
51
|
+
"Skip",
|
|
52
|
+
"blick",
|
|
53
|
+
"build",
|
|
54
|
+
"discover",
|
|
55
|
+
"find_project_root",
|
|
56
|
+
"fingerprint",
|
|
57
|
+
"group",
|
|
58
|
+
"load_config",
|
|
59
|
+
"plots",
|
|
60
|
+
"register",
|
|
61
|
+
"slug",
|
|
62
|
+
]
|
blicket/_validation.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Checks of the arguments of ``bk.blick``, ``bk.register`` and ``bk.group``.
|
|
2
|
+
|
|
3
|
+
Every check normalises one value or raises ``_Invalid`` with all that is wrong with it; the
|
|
4
|
+
collectors at the end turn those into lists of problems, each saying what to do.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import annotationlib
|
|
8
|
+
import inspect
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, TypedDict, get_args
|
|
14
|
+
|
|
15
|
+
from blicket.errors import did_you_mean
|
|
16
|
+
from blicket.plots import PlotSpec
|
|
17
|
+
from blicket.schema import ControlKind
|
|
18
|
+
|
|
19
|
+
SEGMENT_RULE = "lowercase letters, digits, '_' and '-', starting with a letter or digit"
|
|
20
|
+
_SEGMENT = re.compile(r"[a-z0-9][a-z0-9_-]*")
|
|
21
|
+
# Ids become file and directory names; these names are reserved on Windows.
|
|
22
|
+
_RESERVED = frozenset(
|
|
23
|
+
{"con", "prn", "aux", "nul"}
|
|
24
|
+
| {f"com{i}" for i in range(1, 10)}
|
|
25
|
+
| {f"lpt{i}" for i in range(1, 10)}
|
|
26
|
+
)
|
|
27
|
+
_JSON_HINT = "use strings, numbers, booleans, lists and dicts"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Invalid(ValueError):
|
|
31
|
+
"""A value that cannot be used; ``problems`` say why and what to do."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *problems: str) -> None:
|
|
34
|
+
super().__init__("; ".join(problems))
|
|
35
|
+
self.problems = problems
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BlickOptions(TypedDict, total=False):
|
|
39
|
+
"""The options of ``@bk.blick`` and ``bk.register``."""
|
|
40
|
+
|
|
41
|
+
id: str
|
|
42
|
+
title: str
|
|
43
|
+
description: str
|
|
44
|
+
unit: str
|
|
45
|
+
plot: PlotSpec
|
|
46
|
+
independent_y: bool
|
|
47
|
+
controls: Mapping[str, ControlKind]
|
|
48
|
+
defaults: Mapping[str, Any]
|
|
49
|
+
tolerance: float
|
|
50
|
+
aliases: Iterable[str]
|
|
51
|
+
tags: Iterable[str]
|
|
52
|
+
dims: Iterable[str]
|
|
53
|
+
measures: Iterable[str]
|
|
54
|
+
order: Mapping[str, Iterable[Any]]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class CheckedOptions:
|
|
59
|
+
"""The valid options of a definition as ``Blick`` fields, empty where not given or invalid."""
|
|
60
|
+
|
|
61
|
+
id: str | None = None
|
|
62
|
+
title: str | None = None
|
|
63
|
+
description: str | None = None
|
|
64
|
+
unit: str | None = None
|
|
65
|
+
plot: PlotSpec | None = None
|
|
66
|
+
independent_y: bool = False
|
|
67
|
+
controls: Mapping[str, ControlKind] = field(default_factory=dict)
|
|
68
|
+
defaults: Mapping[str, Any] = field(default_factory=dict)
|
|
69
|
+
tolerance: float | None = None
|
|
70
|
+
aliases: tuple[str, ...] = ()
|
|
71
|
+
tags: tuple[str, ...] = ()
|
|
72
|
+
dims: tuple[str, ...] | None = None
|
|
73
|
+
measures: tuple[str, ...] | None = None
|
|
74
|
+
order: Mapping[str, tuple[Any, ...]] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def valid_segment(text: str) -> bool:
|
|
78
|
+
"""Whether ``text`` can be one segment of an id or group name."""
|
|
79
|
+
return bool(_SEGMENT.fullmatch(text)) and text not in _RESERVED
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reserved_name(what: str, value: str, segment: str) -> str:
|
|
83
|
+
"""The message for a name with a segment that is a reserved file name on Windows."""
|
|
84
|
+
return f"{what} {value!r} has the segment {segment!r}, a reserved name on Windows"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def default_title(name: str) -> str:
|
|
88
|
+
"""``steel_by_process`` → ``Steel by process``."""
|
|
89
|
+
words = " ".join(part for part in re.split(r"[_-]+", name) if part)
|
|
90
|
+
return words[:1].upper() + words[1:]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _shown(value: object) -> str:
|
|
94
|
+
"""A value for a message; callables by name, since their repr holds a memory address."""
|
|
95
|
+
if callable(value):
|
|
96
|
+
return f"the callable {getattr(value, '__qualname__', type(value).__name__)}"
|
|
97
|
+
return repr(value)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------------------------
|
|
101
|
+
# Checks of single values; each raises _Invalid
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def reserved_segment(value: str) -> str | None:
|
|
105
|
+
"""The first segment of ``value`` that is a reserved file name on Windows, if any."""
|
|
106
|
+
return next((s for s in value.split("/") if s in _RESERVED), None)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _id(value: object, what: str = "id") -> str:
|
|
110
|
+
if isinstance(value, str):
|
|
111
|
+
segments = value.split("/")
|
|
112
|
+
if len(segments) >= 2 and all(valid_segment(s) for s in segments):
|
|
113
|
+
return value
|
|
114
|
+
if (reserved := reserved_segment(value)) is not None:
|
|
115
|
+
raise _Invalid(reserved_name(what, value, reserved))
|
|
116
|
+
raise _Invalid(
|
|
117
|
+
f"{what} {_shown(value)} is not valid: lowercase letters, digits, '_' and '-' in two or "
|
|
118
|
+
"more '/'-separated segments"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _group_name(value: object) -> str:
|
|
123
|
+
if isinstance(value, str):
|
|
124
|
+
if all(valid_segment(s) for s in value.split("/")):
|
|
125
|
+
return value
|
|
126
|
+
if (reserved := reserved_segment(value)) is not None:
|
|
127
|
+
raise _Invalid(reserved_name("group name", value, reserved))
|
|
128
|
+
raise _Invalid(
|
|
129
|
+
f"group name {_shown(value)} is not valid: one or more segments separated by '/', each "
|
|
130
|
+
f"made of {SEGMENT_RULE}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _strings(value: object, what: str) -> tuple[str, ...]:
|
|
135
|
+
"""A collection of non-empty strings. A bare string is rejected: ``tags="mfa"`` is a typo."""
|
|
136
|
+
if isinstance(value, str) or not isinstance(value, Iterable):
|
|
137
|
+
raise _Invalid(f"{what} must be a list of strings, got {_shown(value)}")
|
|
138
|
+
items = tuple(value)
|
|
139
|
+
bad = [_shown(item) for item in items if not isinstance(item, str) or not item]
|
|
140
|
+
if bad:
|
|
141
|
+
raise _Invalid(f"{what} must be a list of non-empty strings, got {', '.join(bad)} in it")
|
|
142
|
+
return items
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _title(value: object, what: str = "title") -> str:
|
|
146
|
+
if not isinstance(value, str) or not value.strip():
|
|
147
|
+
raise _Invalid(f"{what} must be a non-empty string, got {_shown(value)}")
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _transform(value: object, what: str) -> None:
|
|
152
|
+
if not callable(value):
|
|
153
|
+
raise _Invalid(f"{what} must be a function of the run, got {_shown(value)}")
|
|
154
|
+
try:
|
|
155
|
+
# A name bound only under TYPE_CHECKING stays a ForwardRef instead of raising.
|
|
156
|
+
signature = inspect.signature(value, annotation_format=annotationlib.Format.FORWARDREF)
|
|
157
|
+
except ValueError: # some builtins, such as str, have no signature; nothing to check
|
|
158
|
+
return
|
|
159
|
+
try:
|
|
160
|
+
signature.bind(None)
|
|
161
|
+
except TypeError:
|
|
162
|
+
raise _Invalid(
|
|
163
|
+
f"{what} must take exactly one positional parameter, the run; "
|
|
164
|
+
f"its signature is {signature}"
|
|
165
|
+
) from None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _json_issues(value: object, where: str) -> Iterator[str]:
|
|
169
|
+
"""Every part of ``value`` that cannot be written as JSON."""
|
|
170
|
+
if value is None or isinstance(value, bool | int | str):
|
|
171
|
+
return
|
|
172
|
+
if isinstance(value, float):
|
|
173
|
+
if not math.isfinite(value):
|
|
174
|
+
yield f"{where} is {value!r}, a number that JSON cannot hold; {_JSON_HINT}"
|
|
175
|
+
elif isinstance(value, list | tuple):
|
|
176
|
+
for index, item in enumerate(value):
|
|
177
|
+
yield from _json_issues(item, f"{where}[{index}]")
|
|
178
|
+
elif isinstance(value, Mapping):
|
|
179
|
+
for key, item in value.items():
|
|
180
|
+
if isinstance(key, str):
|
|
181
|
+
yield from _json_issues(item, f"{where}[{key!r}]")
|
|
182
|
+
else:
|
|
183
|
+
yield f"{where} has the key {key!r}; keys must be strings"
|
|
184
|
+
else:
|
|
185
|
+
yield f"{where} is of type {type(value).__name__}, which JSON cannot hold; {_JSON_HINT}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _json_mapping(value: object, what: str, shape: str) -> Mapping[Any, Any]:
|
|
189
|
+
if not isinstance(value, Mapping):
|
|
190
|
+
raise _Invalid(f"{what} must be a dict of {shape}, got {_shown(value)}")
|
|
191
|
+
if issues := list(_json_issues(value, what)):
|
|
192
|
+
raise _Invalid(*issues)
|
|
193
|
+
return value
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _description(value: object) -> str:
|
|
197
|
+
if not isinstance(value, str):
|
|
198
|
+
raise _Invalid(f"description must be a string, got {_shown(value)}")
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _plot(value: object) -> PlotSpec:
|
|
203
|
+
if not isinstance(value, PlotSpec):
|
|
204
|
+
raise _Invalid(
|
|
205
|
+
f"plot must be a plot spec such as bk.plots.line(x=..., y=...), got {_shown(value)}"
|
|
206
|
+
)
|
|
207
|
+
return value
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _flag(value: object) -> bool:
|
|
211
|
+
if not isinstance(value, bool):
|
|
212
|
+
raise _Invalid(f"independent_y must be True or False, got {_shown(value)}")
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _controls(value: object) -> dict[str, ControlKind]:
|
|
217
|
+
if not isinstance(value, Mapping):
|
|
218
|
+
raise _Invalid(
|
|
219
|
+
f"controls must be a dict of column to 'filter' or 'split', got {_shown(value)}"
|
|
220
|
+
)
|
|
221
|
+
kinds = get_args(ControlKind)
|
|
222
|
+
issues = [
|
|
223
|
+
f"controls[{column!r}] is {_shown(kind)}; use a column name and 'filter' or 'split'"
|
|
224
|
+
for column, kind in value.items()
|
|
225
|
+
if not isinstance(column, str) or kind not in kinds
|
|
226
|
+
]
|
|
227
|
+
if issues:
|
|
228
|
+
raise _Invalid(*issues)
|
|
229
|
+
return dict(value)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _tolerance(value: object) -> float:
|
|
233
|
+
if isinstance(value, int | float) and not isinstance(value, bool) and 0 < value < 1:
|
|
234
|
+
return float(value)
|
|
235
|
+
raise _Invalid(f"tolerance must be a positive number below 1, got {_shown(value)}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _aliases(value: object) -> tuple[str, ...]:
|
|
239
|
+
aliases = _strings(value, "aliases")
|
|
240
|
+
issues: list[str] = []
|
|
241
|
+
for alias in aliases:
|
|
242
|
+
_collect(issues, _id, alias, "alias")
|
|
243
|
+
if len(set(aliases)) != len(aliases):
|
|
244
|
+
issues.append(f"aliases lists an id twice: {list(aliases)}")
|
|
245
|
+
if issues:
|
|
246
|
+
raise _Invalid(*issues)
|
|
247
|
+
return aliases
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _order(value: object) -> dict[str, tuple[Any, ...]]:
|
|
251
|
+
order = _json_mapping(value, "order", "column to a list of values")
|
|
252
|
+
issues = [
|
|
253
|
+
f"order[{key!r}] must be a list of values in display order"
|
|
254
|
+
for key, values in order.items()
|
|
255
|
+
if not isinstance(values, list | tuple)
|
|
256
|
+
]
|
|
257
|
+
if issues:
|
|
258
|
+
raise _Invalid(*issues)
|
|
259
|
+
return {key: tuple(values) for key, values in order.items()}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# One check per option of BlickOptions; test_definitions holds them to the same keys.
|
|
263
|
+
_CHECKS: dict[str, Callable[[object], Any]] = {
|
|
264
|
+
"id": _id,
|
|
265
|
+
"title": _title,
|
|
266
|
+
"description": _description,
|
|
267
|
+
"unit": lambda value: _title(value, "unit"),
|
|
268
|
+
"plot": _plot,
|
|
269
|
+
"independent_y": _flag,
|
|
270
|
+
"controls": _controls,
|
|
271
|
+
"defaults": lambda value: dict(_json_mapping(value, "defaults", "column to value")),
|
|
272
|
+
"tolerance": _tolerance,
|
|
273
|
+
"aliases": _aliases,
|
|
274
|
+
"tags": lambda value: tuple(dict.fromkeys(_strings(value, "tags"))),
|
|
275
|
+
"dims": lambda value: _strings(value, "dims"),
|
|
276
|
+
"measures": lambda value: _strings(value, "measures"),
|
|
277
|
+
"order": _order,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------------------------
|
|
282
|
+
# Collectors
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _collect[T](problems: list[str], check: Callable[..., T], *args: object) -> T | None:
|
|
286
|
+
"""Run ``check``; on ``_Invalid`` add its problems and return ``None``."""
|
|
287
|
+
try:
|
|
288
|
+
return check(*args)
|
|
289
|
+
except _Invalid as error:
|
|
290
|
+
problems.extend(error.problems)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def validate_definition(
|
|
295
|
+
options: Mapping[str, object], transform: object, transform_label: str
|
|
296
|
+
) -> tuple[CheckedOptions, list[str]]:
|
|
297
|
+
"""Check the options of a definition and its transform, and collect every problem.
|
|
298
|
+
|
|
299
|
+
An option passed as ``None`` counts as not given. ``transform`` is ``None`` when missing.
|
|
300
|
+
"""
|
|
301
|
+
values: dict[str, Any] = {}
|
|
302
|
+
problems: list[str] = []
|
|
303
|
+
for key, value in options.items():
|
|
304
|
+
if key not in _CHECKS:
|
|
305
|
+
problems.append(_unknown_option(key))
|
|
306
|
+
elif value is not None:
|
|
307
|
+
values[key] = _collect(problems, _CHECKS[key], value)
|
|
308
|
+
if options.get("plot") is None:
|
|
309
|
+
problems.append("plot= is missing; pass a plot spec such as bk.plots.line(x=..., y=...)")
|
|
310
|
+
if transform is None:
|
|
311
|
+
problems.append("transform= is missing; pass a function that takes the run")
|
|
312
|
+
else:
|
|
313
|
+
_collect(problems, _transform, transform, transform_label)
|
|
314
|
+
checked = {key: value for key, value in values.items() if value is not None}
|
|
315
|
+
return CheckedOptions(**checked), problems
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _unknown_option(key: str) -> str:
|
|
319
|
+
if key == "transform":
|
|
320
|
+
return (
|
|
321
|
+
"transform= belongs to bk.register(); with @bk.blick the decorated function is the "
|
|
322
|
+
"transform"
|
|
323
|
+
)
|
|
324
|
+
close = did_you_mean(key, _CHECKS)
|
|
325
|
+
hint = f"did you mean {close}=?" if close else f"valid: {', '.join(_CHECKS)}"
|
|
326
|
+
return f"unknown option {key}=; {hint}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class GroupArguments:
|
|
331
|
+
"""The checked arguments of ``bk.group()``; ``name`` is ``None`` when it is unusable."""
|
|
332
|
+
|
|
333
|
+
name: str | None
|
|
334
|
+
title: str | None = None
|
|
335
|
+
tags: tuple[str, ...] = ()
|
|
336
|
+
problems: list[str] = field(default_factory=list)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def validate_group(name: object, title: object = None, tags: object = ()) -> GroupArguments:
|
|
340
|
+
"""Check the arguments of ``bk.group()``, or a group name derived from a module name."""
|
|
341
|
+
problems: list[str] = []
|
|
342
|
+
checked_name = _collect(problems, _group_name, name)
|
|
343
|
+
checked_title = None if title is None else _collect(problems, _title, title, "the group title")
|
|
344
|
+
checked_tags = _collect(problems, _strings, tags, "tags") or ()
|
|
345
|
+
return GroupArguments(checked_name, checked_title, tuple(dict.fromkeys(checked_tags)), problems)
|