kalfa 3.0.0.dev0__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.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from kalfa.commands.guide.words import and_list, unique, value_text
|
|
6
|
+
from kalfa.core import aliases, spec
|
|
7
|
+
from kalfa.core.build import signature_of
|
|
8
|
+
from kalfa.core.refs import SINGULAR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TABLES = ("models", "losses", "metrics", "optimizers", "plots", "rules")
|
|
12
|
+
LAYER_LABELS = ("--set", "-p", "mapping")
|
|
13
|
+
INDEX = re.compile(r"(\[\d+\])+$")
|
|
14
|
+
PARTS = re.compile(r"[.\[]")
|
|
15
|
+
DRAWN = {"log": "log uniform", "float": "uniform", "int": "integers"}
|
|
16
|
+
STEPPED = {"log": "values in geometric steps", "float": "values in even steps", "int": "integers in even steps"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def raw_of(config):
|
|
20
|
+
merged = {}
|
|
21
|
+
for layer in getattr(config, "layers", None) or ():
|
|
22
|
+
if isinstance(layer.data, dict):
|
|
23
|
+
spec.merge(merged, layer.data, "", [], {}, layer.label)
|
|
24
|
+
return merged
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def walk(value, path, found, pick):
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
for key, item in value.items():
|
|
30
|
+
walk(item, spec.join(path, key), found, pick)
|
|
31
|
+
elif isinstance(value, list):
|
|
32
|
+
for index, item in enumerate(value):
|
|
33
|
+
walk(item, spec.join(path, index), found, pick)
|
|
34
|
+
elif isinstance(value, str):
|
|
35
|
+
found.extend((path, item) for item in pick(value))
|
|
36
|
+
return found
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def tokens(text):
|
|
40
|
+
return [match.group(1) for match in spec.TOKEN.finditer(text) if match.group(1)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tokens_in(value):
|
|
44
|
+
return unique(name for _, name in walk(value, "", [], tokens))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def uses_of(raw):
|
|
48
|
+
uses = {}
|
|
49
|
+
for path, name in walk(raw, "", [], tokens):
|
|
50
|
+
uses.setdefault(name.split(".")[0], []).append(INDEX.sub("", path))
|
|
51
|
+
return {name: unique(paths) for name, paths in uses.items()}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def refs_of(data):
|
|
55
|
+
return walk(data, "", [], lambda text: [text] if text.startswith("@") else [])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ref_name(value, section):
|
|
59
|
+
lead = f"@{section}."
|
|
60
|
+
return value[len(lead):].split(".")[0] if isinstance(value, str) and value.startswith(lead) else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ref_meaning(text):
|
|
64
|
+
parts = str(text)[1:].split(".")
|
|
65
|
+
root, rest = parts[0], parts[1:]
|
|
66
|
+
if root in SINGULAR and rest:
|
|
67
|
+
kind, name, inner = SINGULAR[root], rest[0], rest[1:]
|
|
68
|
+
if not inner:
|
|
69
|
+
return f"the {kind} {name}"
|
|
70
|
+
if root == "models" and inner[0] == "ema":
|
|
71
|
+
tail = f" (its {'.'.join(inner[1:])})" if inner[1:] else ""
|
|
72
|
+
return f"the EMA copy of the model {name}{tail}"
|
|
73
|
+
if root == "models":
|
|
74
|
+
return f"the submodule {'.'.join(inner)} of the model {name}"
|
|
75
|
+
return f"the attribute {'.'.join(inner)} of the {kind} {name}"
|
|
76
|
+
if root == "data":
|
|
77
|
+
return f"{'.'.join(rest)}, which the data class exposes once it is fitted" if rest else "the data object"
|
|
78
|
+
if parts == ["generate"]:
|
|
79
|
+
return "the sampler of the generate section"
|
|
80
|
+
if parts == ["train", "step"]:
|
|
81
|
+
return "the step object"
|
|
82
|
+
return f"the node {'.'.join(parts)}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def holder_of(where):
|
|
86
|
+
parts = [part for part in PARTS.split(where) if part]
|
|
87
|
+
if parts and parts[0] in TABLES + ("train",) and len(parts) > 1:
|
|
88
|
+
return ".".join(parts[:2])
|
|
89
|
+
return parts[0] if parts else where
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def quiet(where):
|
|
93
|
+
if where.startswith("optimizers.") and (".over" in where or where.endswith(".minimizes")):
|
|
94
|
+
return True
|
|
95
|
+
return where == "train.predicts" or (where.startswith("rules.") and ".set" in where)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def grid_of(raw):
|
|
99
|
+
from kalfa.commands.sweep.strategies import Grid, strategy_call, strategy_target
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
use, options = strategy_call(raw)
|
|
103
|
+
target = strategy_target(use)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
if not (inspect.isclass(target) and issubclass(target, Grid)):
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
return target(**options)
|
|
110
|
+
except Exception:
|
|
111
|
+
return Grid()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def default_of(target, name):
|
|
115
|
+
if target is None:
|
|
116
|
+
return None
|
|
117
|
+
parameter = signature_of(target).params.get(name)
|
|
118
|
+
if parameter is None or parameter.default is inspect.Parameter.empty:
|
|
119
|
+
return None
|
|
120
|
+
return parameter.default
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Seen:
|
|
124
|
+
def __init__(self, checked, paths=()):
|
|
125
|
+
self.config = checked.config
|
|
126
|
+
self.tree = checked.tree
|
|
127
|
+
self.layers = [layer for layer in getattr(self.config, "layers", None) or () if isinstance(layer.data, dict)]
|
|
128
|
+
self.main = str(Path(paths[0]).expanduser().resolve()) if paths else None
|
|
129
|
+
self.raw = raw_of(self.config)
|
|
130
|
+
data = getattr(self.config, "data", None)
|
|
131
|
+
self.data = data if isinstance(data, dict) else {}
|
|
132
|
+
params = getattr(self.config, "params", None)
|
|
133
|
+
self.params = params if isinstance(params, dict) else {}
|
|
134
|
+
self.uses = uses_of(self.raw)
|
|
135
|
+
self.refs = refs_of(self.data)
|
|
136
|
+
self.grid = grid_of(self.mapping("sweep").get("strategy")) if self.space() else None
|
|
137
|
+
|
|
138
|
+
def value(self, *keys):
|
|
139
|
+
current, rest = (self.params, keys[1:]) if keys and keys[0] == "params" else (self.data, keys)
|
|
140
|
+
for key in rest:
|
|
141
|
+
if not isinstance(current, dict) or key not in current:
|
|
142
|
+
return None
|
|
143
|
+
current = current[key]
|
|
144
|
+
return current
|
|
145
|
+
|
|
146
|
+
def mapping(self, *keys):
|
|
147
|
+
found = self.value(*keys)
|
|
148
|
+
return found if isinstance(found, dict) else {}
|
|
149
|
+
|
|
150
|
+
def node(self, path):
|
|
151
|
+
return self.tree.nodes.get(path) if self.tree is not None else None
|
|
152
|
+
|
|
153
|
+
def target(self, path):
|
|
154
|
+
node = self.node(path)
|
|
155
|
+
return node.target if node is not None else None
|
|
156
|
+
|
|
157
|
+
def tabled(self):
|
|
158
|
+
return getattr(self.target("data"), "predictions", True) is not False
|
|
159
|
+
|
|
160
|
+
def found(self, use):
|
|
161
|
+
if self.tree is None or not aliases.is_import_path(use):
|
|
162
|
+
return None
|
|
163
|
+
return next((node.target for node in self.tree.nodes.values() if node.use == use and node.target is not None),
|
|
164
|
+
None)
|
|
165
|
+
|
|
166
|
+
def predicts(self):
|
|
167
|
+
models = self.mapping("models")
|
|
168
|
+
asked = self.mapping("train").get("predicts")
|
|
169
|
+
if asked is False:
|
|
170
|
+
return None
|
|
171
|
+
if asked is None:
|
|
172
|
+
return next(iter(models)) if len(models) == 1 else None
|
|
173
|
+
name = ref_name(asked, "models")
|
|
174
|
+
return name if name in models else None
|
|
175
|
+
|
|
176
|
+
def first_output(self, model):
|
|
177
|
+
outputs = self.mapping("models", model).get("outputs")
|
|
178
|
+
if isinstance(outputs, str):
|
|
179
|
+
return outputs
|
|
180
|
+
return str(outputs[0]) if isinstance(outputs, list) and outputs else model
|
|
181
|
+
|
|
182
|
+
def targets(self):
|
|
183
|
+
data = self.mapping("data")
|
|
184
|
+
targets = data.get("targets")
|
|
185
|
+
if isinstance(targets, dict):
|
|
186
|
+
return [str(name) for name in targets]
|
|
187
|
+
found = aliases.target_path(self.target("data")) if self.target("data") is not None else ""
|
|
188
|
+
if found.endswith(".ImageFolder"):
|
|
189
|
+
return ["label"] if data.get("labels", True) is not False else []
|
|
190
|
+
return ["targets"] if found.endswith(".TextLines") else None
|
|
191
|
+
|
|
192
|
+
def minimizers(self, loss):
|
|
193
|
+
return [name for name, item in self.mapping("optimizers").items()
|
|
194
|
+
if isinstance(item, dict) and ref_name(item.get("minimizes"), "losses") == loss]
|
|
195
|
+
|
|
196
|
+
def patches(self):
|
|
197
|
+
for name, rule in self.mapping("rules").items():
|
|
198
|
+
if isinstance(rule, dict) and isinstance(rule.get("set"), dict):
|
|
199
|
+
for path, value in rule["set"].items():
|
|
200
|
+
yield str(name), str(path), value
|
|
201
|
+
|
|
202
|
+
def readers(self, path):
|
|
203
|
+
holders = [holder_of(where) for where, text in self.refs
|
|
204
|
+
if (text[1:] == path or text[1:].startswith(path + ".")) and not quiet(where)]
|
|
205
|
+
return [holder for holder in unique(holders) if holder != path]
|
|
206
|
+
|
|
207
|
+
def space(self):
|
|
208
|
+
space = self.mapping("sweep").get("space")
|
|
209
|
+
return space if isinstance(space, dict) else {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def grid_count(grid, entry):
|
|
213
|
+
try:
|
|
214
|
+
return len(grid.axes({"range": entry})["range"])
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def range_text(entry, grid=None):
|
|
220
|
+
if isinstance(entry, list):
|
|
221
|
+
return and_list(value_text(item) for item in entry)
|
|
222
|
+
if isinstance(entry, dict) and len(entry) == 1:
|
|
223
|
+
kind, bounds = next(iter(entry.items()))
|
|
224
|
+
if isinstance(bounds, list) and len(bounds) == 2:
|
|
225
|
+
ends = f"from {value_text(bounds[0])} to {value_text(bounds[1])}"
|
|
226
|
+
if grid is None or kind not in STEPPED:
|
|
227
|
+
return f"{DRAWN.get(kind, kind)} {ends}"
|
|
228
|
+
count = grid_count(grid, entry)
|
|
229
|
+
if count == 1:
|
|
230
|
+
return f"only {value_text(bounds[0])}"
|
|
231
|
+
return f"{count} {STEPPED[kind]} {ends}" if count else f"{STEPPED[kind]} {ends}"
|
|
232
|
+
return value_text(entry)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def swept(seen, name):
|
|
236
|
+
space = seen.space()
|
|
237
|
+
if name not in space:
|
|
238
|
+
return None
|
|
239
|
+
entry = space[name]
|
|
240
|
+
verb = "tries" if isinstance(entry, list) or seen.grid is not None else "draws"
|
|
241
|
+
return f"the sweep {verb} {range_text(entry, seen.grid)}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def param_value_notes(seen, value):
|
|
245
|
+
notes = []
|
|
246
|
+
for name in tokens_in(value):
|
|
247
|
+
base, _, field = name.partition(".")
|
|
248
|
+
if base == "datetime" and base not in seen.params:
|
|
249
|
+
notes.append("$datetime$ is the start time of the run, such as 20261109_143000")
|
|
250
|
+
continue
|
|
251
|
+
if base not in seen.params:
|
|
252
|
+
continue
|
|
253
|
+
found = seen.params[base]
|
|
254
|
+
if field:
|
|
255
|
+
found = found.get(field) if isinstance(found, dict) else None
|
|
256
|
+
sweep = swept(seen, base) if not field else None
|
|
257
|
+
notes.append(f"${name}$ is {value_text(found)}" + (f" here; {sweep}" if sweep else ""))
|
|
258
|
+
return notes
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def layer_name(label):
|
|
262
|
+
return label if label in LAYER_LABELS else Path(str(label)).name
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def dig(data, keys):
|
|
266
|
+
current = data
|
|
267
|
+
for key in keys:
|
|
268
|
+
if not isinstance(current, dict) or key not in current:
|
|
269
|
+
return False, None
|
|
270
|
+
current = current[key]
|
|
271
|
+
return True, current
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def layer_notes(seen, keys, value):
|
|
275
|
+
found = [layer for layer in seen.layers if dig(layer.data, keys)[0]]
|
|
276
|
+
if not found or [layer.file for layer in found] == [seen.main]:
|
|
277
|
+
return []
|
|
278
|
+
names = unique(layer_name(layer.label) for layer in found)
|
|
279
|
+
if dig(found[-1].data, keys)[1] != value:
|
|
280
|
+
return [f"merged from {and_list(names)}"]
|
|
281
|
+
if len(found) > 1:
|
|
282
|
+
return [f"{names[-1]} writes it over {and_list(names[:-1])}"]
|
|
283
|
+
return [f"written by {names[-1]}"]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def override_notes(seen, path):
|
|
287
|
+
notes = []
|
|
288
|
+
for written, label in (getattr(seen.config, "overrides", None) or {}).items():
|
|
289
|
+
if written.startswith((path + ".", path + "[")):
|
|
290
|
+
notes.append(f"{layer_name(label)} writes {written[len(path) + 1:]} over an earlier layer")
|
|
291
|
+
return notes
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def patch_text(path, value):
|
|
295
|
+
if isinstance(value, dict) and set(value) == {"times"}:
|
|
296
|
+
return f"multiplies {path} by {value_text(value['times'])}"
|
|
297
|
+
if isinstance(value, dict) and set(value) == {"plus"}:
|
|
298
|
+
return f"adds {value_text(value['plus'])} to {path}"
|
|
299
|
+
return f"sets {path} to {value_text(value)}"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def patch_notes(seen, path):
|
|
303
|
+
notes = []
|
|
304
|
+
for rule, written, value in seen.patches():
|
|
305
|
+
if written == path or written.startswith((path + ".", path + "[")):
|
|
306
|
+
notes.append(f"rule {rule} {patch_text('it' if written == path else written[len(path) + 1:], value)}")
|
|
307
|
+
return notes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import textwrap
|
|
2
|
+
|
|
3
|
+
from rich.console import Group
|
|
4
|
+
from rich.padding import Padding
|
|
5
|
+
from rich.syntax import Syntax
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from kalfa.commands.guide.about import RUN, RUN_KEYS
|
|
9
|
+
from kalfa.commands.guide.seen import PARTS
|
|
10
|
+
from kalfa.commands.guide.words import and_list, flat
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TEXT_WIDTH = 100
|
|
14
|
+
THEME = "ansi_dark"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def status_text(doc):
|
|
18
|
+
errors = sum(problem["level"] == "error" for problem in doc["problems"])
|
|
19
|
+
warnings = len(doc["problems"]) - errors
|
|
20
|
+
pairs = ((errors, "error"), (warnings, "warning"))
|
|
21
|
+
counts = [f"{count} {word}{'s' if count != 1 else ''}" for count, word in pairs if count]
|
|
22
|
+
if not counts:
|
|
23
|
+
return "ok"
|
|
24
|
+
return ("ok, " if errors == 0 else "") + and_list(counts)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def problem_line(problem, own=None):
|
|
28
|
+
where = f"{problem['path']}: " if problem.get("path") and problem["path"] != own else ""
|
|
29
|
+
hint = f" ({problem['hint']})" if problem.get("hint") else ""
|
|
30
|
+
return f"{problem['level']} {problem['code']}: {where}{problem['message']}{hint}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def section_name(path):
|
|
34
|
+
if not path:
|
|
35
|
+
return None
|
|
36
|
+
first = PARTS.split(path)[0]
|
|
37
|
+
return RUN if first in RUN_KEYS else first
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def placed(doc):
|
|
41
|
+
attached = [problem for section in doc["sections"] for entry in section["entries"]
|
|
42
|
+
for problem in entry["problems"]]
|
|
43
|
+
names = {section["name"] for section in doc["sections"]}
|
|
44
|
+
own, loose = {}, []
|
|
45
|
+
for problem in doc["problems"]:
|
|
46
|
+
if problem in attached:
|
|
47
|
+
continue
|
|
48
|
+
name = section_name(problem.get("path"))
|
|
49
|
+
if name in names:
|
|
50
|
+
own.setdefault(name, []).append(problem)
|
|
51
|
+
else:
|
|
52
|
+
loose.append(problem)
|
|
53
|
+
return own, loose
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def wrapped(text, lead, rest=None):
|
|
57
|
+
rest = lead if rest is None else rest
|
|
58
|
+
return textwrap.wrap(flat(text), width=TEXT_WIDTH, initial_indent=lead, subsequent_indent=rest,
|
|
59
|
+
break_long_words=False, break_on_hyphens=False) or [lead.rstrip()]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def text(doc):
|
|
63
|
+
own, loose = placed(doc)
|
|
64
|
+
lines = [f"{doc['title']}: {status_text(doc)}"]
|
|
65
|
+
lines += [line for problem in loose for line in wrapped(problem_line(problem), " ", " ")]
|
|
66
|
+
for section in doc["sections"]:
|
|
67
|
+
lines += ["", section["name"], "=" * len(section["name"])] + wrapped(section["about"], "")
|
|
68
|
+
lines += [line for problem in own.get(section["name"], []) for line in wrapped(problem_line(problem), " ")]
|
|
69
|
+
for entry in section["entries"]:
|
|
70
|
+
lines.append("")
|
|
71
|
+
lines += [" " + line for line in entry["yaml"].splitlines()]
|
|
72
|
+
lines += [line for note in entry["notes"] for line in wrapped(note, " ")]
|
|
73
|
+
lines += [line for problem in entry["problems"]
|
|
74
|
+
for line in wrapped(problem_line(problem, entry["path"]), " ", " ")]
|
|
75
|
+
return "\n".join(lines)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def problem_text(problem, own=None):
|
|
79
|
+
return Text(problem_line(problem, own), style="red" if problem["level"] == "error" else "yellow")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def head_renderable(doc):
|
|
83
|
+
_, loose = placed(doc)
|
|
84
|
+
style = "bold green" if doc["ok"] and not doc["problems"] else "bold yellow" if doc["ok"] else "bold red"
|
|
85
|
+
title = Text.assemble((doc["title"], "bold"), ": ", (status_text(doc), style))
|
|
86
|
+
return Group(title, *(Padding(problem_text(problem), (0, 0, 0, 2)) for problem in loose))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def entry_renderable(entry):
|
|
90
|
+
parts = [Syntax(entry["yaml"], "yaml", theme=THEME, background_color="default", word_wrap=True)]
|
|
91
|
+
if entry["notes"]:
|
|
92
|
+
parts.append(Padding(Text("\n".join(entry["notes"]), style="dim"), (0, 0, 0, 4)))
|
|
93
|
+
parts += [Padding(problem_text(problem, entry["path"]), (0, 0, 0, 4)) for problem in entry["problems"]]
|
|
94
|
+
return Padding(Group(*parts), (1, 0, 0, 2))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def section_renderable(section, doc=None):
|
|
98
|
+
own, _ = placed(doc) if doc is not None else ({}, [])
|
|
99
|
+
bar = Padding(Text(section["name"], style="bold"), (0, 1), style=f"bold white on {section['color']}", expand=True)
|
|
100
|
+
about = Padding(Text(section["about"], style="italic"), (0, 1))
|
|
101
|
+
problems = [Padding(problem_text(problem), (0, 0, 0, 2)) for problem in own.get(section["name"], [])]
|
|
102
|
+
return Group(bar, about, *problems, *(entry_renderable(entry) for entry in section["entries"]), Text(""))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def renderable(doc):
|
|
106
|
+
return Group(head_renderable(doc), Text(""), *(section_renderable(section, doc) for section in doc["sections"]))
|