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.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,508 @@
1
+ import importlib
2
+ import inspect
3
+ import json
4
+ import textwrap
5
+ from pathlib import Path
6
+
7
+ from rich.console import Group
8
+ from rich.padding import Padding
9
+ from rich.syntax import Syntax
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+ from ruamel.yaml import YAML
13
+ from ruamel.yaml.error import YAMLError
14
+
15
+ from kalfa.core import aliases
16
+ from kalfa.core.build import node_signature
17
+ from kalfa.core.errors import BuildError, KalfaError
18
+ from kalfa.std import notes
19
+
20
+
21
+ SLOT_ORDER = ("data", "model", "loss", "metric", "optimizer", "schedule", "step", "trigger", "action", "logger",
22
+ "sampler", "plot", "strategy", "value")
23
+ UNKNOWN = "unknown"
24
+ SWEEP_MODULE = "kalfa.commands.sweep"
25
+ REGISTRARS = (SWEEP_MODULE,)
26
+ REQUIRED = "required"
27
+ WIDTH = 100
28
+ NOTE_INDENT = " "
29
+ THEME = "ansi_dark"
30
+ PARENTS = Path(__file__).resolve().parents
31
+ ROOT = PARENTS[3] if PARENTS[2].name == "src" else None
32
+ EXAMPLES = "examples/*/*.yaml"
33
+ SECTIONS = ("models", "losses", "metrics", "optimizers", "plots")
34
+ SINGLE = ("data", "generate")
35
+ COLUMNS = ("features", "targets")
36
+ LEGO_KEYS = ("activation", "init", "schedule", "step", "strategy", "tokenizer", "when", "feed", "run", "log", "layers",
37
+ "each", "split", "transform", "train_transform")
38
+ SEEN = {}
39
+ CATEGORIES = (
40
+ ("data", "data", "slot", ("data",)),
41
+ ("splits", "data.split", "module", ("kalfa.std.data.splits",)),
42
+ ("table preprocessing", "data.features and data.targets", "module",
43
+ ("kalfa.std.data.pre", "sklearn.preprocessing")),
44
+ ("feeds", "data.feed", "module", ("kalfa.std.data.window",)),
45
+ ("image transforms", "data.transform and data.train_transform", "module", ("kalfa.std.data.transforms",)),
46
+ ("tokenizers", "data.tokenizer", "module", ("kalfa.std.data.text.CharTokenizer",)),
47
+ ("models", "models.<name>", "slot", ("model",)),
48
+ ("layers", "layers of a stack model", "name",
49
+ ("linear", "conv2d", "batch_norm", "layer_norm", "dropout", "embedding", "flatten", "last_step")),
50
+ ("activations", "the activation of mlp or layers of stack", "name", ("relu", "gelu", "silu", "sigmoid", "tanh")),
51
+ ("init", "models.<name>.init", "module", ("kalfa.std.init",)),
52
+ ("losses", "losses.<name>", "slot", ("loss",)),
53
+ ("objectives", "losses.<name>", "name",
54
+ ("vae_elbo", "wgan_gp_d", "wgan_g", "ddpm", "distill", "ntxent", "mdmm", "weighted_sum")),
55
+ ("noise schedules", "the schedule of ddpm", "name", ("cosine_beta", "linear_beta")),
56
+ ("metrics", "metrics.<name>", "slot", ("metric",)),
57
+ ("optimizers", "optimizers.<name>", "slot", ("optimizer",)),
58
+ ("lr schedules", "optimizers.<name>.schedule", "slot", ("schedule",)),
59
+ ("steps", "train.step", "slot", ("step",)),
60
+ ("triggers", "rules.<name>.when", "slot", ("trigger",)),
61
+ ("actions", "rules.<name>.run", "slot", ("action",)),
62
+ ("samplers", "generate", "slot", ("sampler",)),
63
+ ("plots", "plots.<name>", "slot", ("plot",)),
64
+ ("loggers", "train.log", "slot", ("logger",)),
65
+ ("sweep strategies", "sweep.strategy", "slot", ("strategy",)),
66
+ ("other", None, "slot", ()),
67
+ )
68
+ OTHER = "other"
69
+ PLACES = {category: place for category, place, _, _ in CATEGORIES}
70
+ RULES = ("name", "module", "slot")
71
+ FIELD = 8
72
+ PLUGIN = "(plugin)"
73
+
74
+
75
+ def load_registrars():
76
+ for module in REGISTRARS:
77
+ importlib.import_module(module)
78
+ notes.apply()
79
+
80
+
81
+ def strategy_note(entry):
82
+ target = importlib.import_module(SWEEP_MODULE).STRATEGIES.get(entry.name)
83
+ if target is None or entry.slot == "strategy":
84
+ return None
85
+ path = aliases.target_path(target)
86
+ names = [other.name for other in aliases.catalog(slot="strategy") if aliases.target_path(other.target) == path]
87
+ return f"in a sweep section, {entry.name} is the {names[0]} strategy" if names else None
88
+
89
+
90
+ def group_of(entry):
91
+ return entry.slot or UNKNOWN
92
+
93
+
94
+ def page_entry(name):
95
+ load_registrars()
96
+ return aliases.entry_of(name) if isinstance(name, str) else None
97
+
98
+
99
+ def entry_from(name_or_entry):
100
+ if isinstance(name_or_entry, aliases.Entry):
101
+ return name_or_entry
102
+ return page_entry(name_or_entry)
103
+
104
+
105
+ def target_in(entry, modules):
106
+ path = aliases.target_path(entry.target)
107
+ return any(path == module or path.startswith(module + ".") for module in modules)
108
+
109
+
110
+ def fits(entry, rule, members):
111
+ if rule == "name":
112
+ return entry.name in members
113
+ if rule == "module":
114
+ return target_in(entry, members)
115
+ return entry.slot in members
116
+
117
+
118
+ def category_of(name_or_entry):
119
+ entry = entry_from(name_or_entry)
120
+ if entry is None:
121
+ return OTHER
122
+ for rule in ("slot",) if entry.plugin else RULES:
123
+ for category, _, kind, members in CATEGORIES:
124
+ if kind == rule and fits(entry, rule, members):
125
+ return category
126
+ return OTHER
127
+
128
+
129
+ def place_of(name_or_entry):
130
+ return PLACES.get(category_of(name_or_entry))
131
+
132
+
133
+ def filters(word, slot):
134
+ return (word if isinstance(word, str) and word else None), (str(slot).lower() if slot else None)
135
+
136
+
137
+ def categories(word=None, slot=None):
138
+ load_registrars()
139
+ word, slot = filters(word, slot)
140
+ groups = {}
141
+ for entry in aliases.catalog(word=word):
142
+ if slot is None or group_of(entry) == slot:
143
+ groups.setdefault(category_of(entry), []).append(entry)
144
+ return [(category, groups[category]) for category, *_ in CATEGORIES if category in groups]
145
+
146
+
147
+ def slot_rank(slot):
148
+ if slot in SLOT_ORDER:
149
+ return (0, SLOT_ORDER.index(slot), slot)
150
+ if slot == UNKNOWN:
151
+ return (2, 0, slot)
152
+ return (1, 0, slot)
153
+
154
+
155
+ def flat(text):
156
+ return " ".join(str(text or "").split())
157
+
158
+
159
+ def wrapped(text, lead, rest=None):
160
+ body = flat(text)
161
+ if not body:
162
+ return [lead.rstrip()]
163
+ rest = " " * len(lead) if rest is None else rest
164
+ width = max(WIDTH, len(lead) + 40)
165
+ return textwrap.wrap(body, width=width, initial_indent=lead, subsequent_indent=rest, break_long_words=False,
166
+ break_on_hyphens=False)
167
+
168
+
169
+ def shown(value):
170
+ if inspect.isclass(value) or inspect.isroutine(value) or inspect.ismodule(value):
171
+ return aliases.target_path(value)
172
+ text = repr(value)
173
+ if " at 0x" in text:
174
+ return f"<{aliases.target_path(type(value))}>"
175
+ return text
176
+
177
+
178
+ def parameters(entry):
179
+ target = aliases.import_target(entry.target)
180
+ signature = node_signature(target, entry.slot or "value")
181
+ empty = inspect.Parameter.empty
182
+ return [(name, parameter.default, parameter.default is empty) for name, parameter in signature.params.items()]
183
+
184
+
185
+ def signature_of(entry):
186
+ if isinstance(entry, str):
187
+ load_registrars()
188
+ found = aliases.entry_of(entry)
189
+ if found is None:
190
+ raise KalfaError(f"no name {entry}")
191
+ entry = found
192
+ return [(name, REQUIRED if required else default) for name, default, required in parameters(entry)]
193
+
194
+
195
+ def is_name(text):
196
+ load_registrars()
197
+ return isinstance(text, str) and text in aliases.ALIASES
198
+
199
+
200
+ def present_slots():
201
+ return sorted({group_of(entry) for entry in aliases.ALIASES.values()}, key=slot_rank)
202
+
203
+
204
+ def empty_text(word, slot):
205
+ if word is None:
206
+ if slot is None:
207
+ return "no names are registered"
208
+ return f"no name of slot {slot}; the slots are " + ", ".join(present_slots())
209
+ lines = [f"no name matches {word}" + (f" in slot {slot}" if slot else "")]
210
+ hint = aliases.suggestion(word)
211
+ if hint:
212
+ lines.append(hint)
213
+ return "\n".join(lines)
214
+
215
+
216
+ def listed_doc(entry):
217
+ return f"{PLUGIN} {flat(entry.doc)}".rstrip() if entry.plugin else flat(entry.doc)
218
+
219
+
220
+ def catalog_text(word=None, slot=None):
221
+ word, slot = filters(word, slot)
222
+ found = categories(word, slot)
223
+ if not found:
224
+ return empty_text(word, slot)
225
+ lines = []
226
+ for category, members in found:
227
+ if lines:
228
+ lines.append("")
229
+ lines.append(category)
230
+ width = max(len(entry.name) for entry in members)
231
+ for entry in members:
232
+ lines.extend(wrapped(listed_doc(entry), " " + entry.name.ljust(width) + " "))
233
+ return "\n".join(lines)
234
+
235
+
236
+ def strings(value):
237
+ if isinstance(value, str):
238
+ return [value]
239
+ if isinstance(value, list):
240
+ return [item for item in value if isinstance(item, str)]
241
+ return []
242
+
243
+
244
+ def nested_words(value):
245
+ if isinstance(value, list):
246
+ return [word for item in value for word in nested_words(item)]
247
+ if not isinstance(value, dict):
248
+ return []
249
+ found = []
250
+ for key, item in value.items():
251
+ if key == "use" or key in LEGO_KEYS:
252
+ found.extend(strings(item))
253
+ found.extend(nested_words(item))
254
+ return found
255
+
256
+
257
+ def section_words(tree):
258
+ found = []
259
+ for section in SECTIONS:
260
+ entries = tree.get(section)
261
+ if isinstance(entries, dict):
262
+ found.extend(value for value in entries.values() if isinstance(value, str))
263
+ found.extend(tree[section] for section in SINGLE if isinstance(tree.get(section), str))
264
+ data = tree.get("data")
265
+ if isinstance(data, dict):
266
+ for key in COLUMNS:
267
+ if isinstance(data.get(key), dict):
268
+ found.extend(word for value in data[key].values() for word in strings(value))
269
+ return found
270
+
271
+
272
+ def lego_words(tree):
273
+ if not isinstance(tree, dict):
274
+ return []
275
+ return section_words(tree) + nested_words(tree)
276
+
277
+
278
+ def loaded(path):
279
+ try:
280
+ return YAML(typ="safe", pure=True).load(path.read_text(encoding="utf-8"))
281
+ except (OSError, UnicodeDecodeError, YAMLError):
282
+ return None
283
+
284
+
285
+ def words_in(path):
286
+ try:
287
+ stamp = path.stat().st_mtime_ns
288
+ except OSError:
289
+ return frozenset()
290
+ found = SEEN.get(path)
291
+ if found is None or found[0] != stamp:
292
+ found = (stamp, frozenset(lego_words(loaded(path))))
293
+ SEEN[path] = found
294
+ return found[1]
295
+
296
+
297
+ def example_files():
298
+ if ROOT is None or not (ROOT / "examples").is_dir():
299
+ return []
300
+ return list(ROOT.glob(EXAMPLES))
301
+
302
+
303
+ def used_in(name):
304
+ entry = aliases.entry_of(name) if isinstance(name, str) else None
305
+ spellings = {name} if entry is None else {name, aliases.target_path(entry.target)}
306
+ return sorted(path.relative_to(ROOT).as_posix() for path in example_files() if spellings & set(words_in(path)))
307
+
308
+
309
+ def example_source(example):
310
+ text = example if isinstance(example, str) else json.dumps(example, default=str)
311
+ return text.strip("\n")
312
+
313
+
314
+ def example_lines(source):
315
+ return [" " + line if line.strip() else "" for line in source.splitlines()]
316
+
317
+
318
+ def fields_of(entry):
319
+ category = category_of(entry)
320
+ rows = [("field", "target", aliases.target_path(entry.target)), ("field", "slot", group_of(entry)),
321
+ ("field", "category", category)]
322
+ if PLACES.get(category):
323
+ rows.append(("field", "goes in", PLACES[category]))
324
+ if entry.plugin:
325
+ rows.append(("field", "from", "a plugin"))
326
+ note = strategy_note(entry)
327
+ if note:
328
+ rows.append(("field", "sweep", note))
329
+ return rows
330
+
331
+
332
+ def param_rows(entry):
333
+ try:
334
+ found = parameters(entry)
335
+ except BuildError as exc:
336
+ return [("problem", f"not readable: {exc}")]
337
+ rows = []
338
+ for param, default, required in found:
339
+ rows.append(("param", param, None if required else shown(default)))
340
+ if flat(entry.params.get(param)):
341
+ rows.append(("note", flat(entry.params[param])))
342
+ return rows or [("problem", "none")]
343
+
344
+
345
+ def page_of(entry):
346
+ rows = [("title", entry.name), *fields_of(entry)]
347
+ if flat(entry.doc):
348
+ rows += [("blank",), ("doc", flat(entry.doc))]
349
+ rows += [("blank",), ("heading", "params:"), *param_rows(entry)]
350
+ if entry.examples:
351
+ rows += [("blank",), ("heading", "examples:")]
352
+ for position, example in enumerate(entry.examples):
353
+ if position:
354
+ rows.append(("blank",))
355
+ rows.append(("example", example_source(example)))
356
+ used = used_in(entry.name)
357
+ if used:
358
+ rows += [("blank",), ("heading", "used in:"), *[("path", path) for path in used]]
359
+ return rows
360
+
361
+
362
+ def param_line(row):
363
+ return f" {row[1]} (required)" if row[2] is None else f" {row[1]} = {row[2]}"
364
+
365
+
366
+ TEXT = {
367
+ "title": lambda row: [row[1]],
368
+ "field": lambda row: [f" {row[1].ljust(FIELD)} {row[2]}"],
369
+ "doc": lambda row: wrapped(row[1], " "),
370
+ "heading": lambda row: [row[1]],
371
+ "param": lambda row: [param_line(row)],
372
+ "note": lambda row: wrapped(row[1], NOTE_INDENT),
373
+ "example": lambda row: example_lines(row[1]),
374
+ "path": lambda row: [" " + row[1]],
375
+ "problem": lambda row: [" " + row[1]],
376
+ "blank": lambda row: [""],
377
+ }
378
+
379
+
380
+ def indented(renderable, width):
381
+ return Padding(renderable, (0, 0, 0, width))
382
+
383
+
384
+ def rich_param(row):
385
+ name = (" " + row[1], "bold green")
386
+ if row[2] is None:
387
+ return Text.assemble(name, (" (required)", "red"))
388
+ return Text.assemble(name, (" = ", "dim"), (row[2], "cyan"))
389
+
390
+
391
+ def rich_example(row):
392
+ return indented(Syntax(row[1], "yaml", theme=THEME, background_color="default", word_wrap=True), 2)
393
+
394
+
395
+ RICH = {
396
+ "title": lambda row: Text(row[1], style="bold cyan"),
397
+ "field": lambda row: Text.assemble((f" {row[1].ljust(FIELD)} ", "dim"), row[2]),
398
+ "doc": lambda row: indented(Text(row[1]), 2),
399
+ "heading": lambda row: Text(row[1], style="bold yellow"),
400
+ "param": rich_param,
401
+ "note": lambda row: indented(Text(row[1]), len(NOTE_INDENT)),
402
+ "example": rich_example,
403
+ "path": lambda row: Text(" " + row[1], style="blue"),
404
+ "problem": lambda row: Text(" " + row[1], style="yellow"),
405
+ "blank": lambda row: Text(""),
406
+ }
407
+
408
+
409
+ def missing_text(name):
410
+ lines = [f"no name {name}"]
411
+ hint = aliases.suggestion(name)
412
+ if hint:
413
+ lines.append(hint)
414
+ return "\n".join(lines)
415
+
416
+
417
+ def entry_text(name):
418
+ entry = page_entry(name)
419
+ if entry is None:
420
+ return missing_text(name)
421
+ return "\n".join(line for row in page_of(entry) for line in TEXT[row[0]](row))
422
+
423
+
424
+ def entry_renderable(name):
425
+ entry = page_entry(name)
426
+ if entry is None:
427
+ return Text(missing_text(name))
428
+ return Group(*[RICH[row[0]](row) for row in page_of(entry)])
429
+
430
+
431
+ def entry_label(entry):
432
+ parts = [(entry.name, "bold")]
433
+ if entry.plugin:
434
+ parts.append((" " + PLUGIN, "magenta"))
435
+ if flat(entry.doc):
436
+ parts.append((" " + flat(entry.doc), "dim"))
437
+ return Text.assemble(*parts)
438
+
439
+
440
+ def category_renderable(category, entries):
441
+ rows = [RICH["title"](("title", category))]
442
+ if PLACES.get(category):
443
+ rows.append(RICH["field"](("field", "goes in", PLACES[category])))
444
+ grid = Table.grid(padding=(0, 2))
445
+ grid.add_column(style="bold green", no_wrap=True)
446
+ grid.add_column()
447
+ for entry in entries:
448
+ grid.add_row(Text(entry.name), Text(listed_doc(entry)))
449
+ return Group(*rows, Text(""), indented(grid, 2))
450
+
451
+
452
+ def prose(text):
453
+ return flat(text).replace("<", "\\<").replace("*", "\\*")
454
+
455
+
456
+ def markdown_param(entry, param, default, required):
457
+ head = f"`{param}` (required)" if required else f"`{param}={shown(default)}`"
458
+ note = prose(entry.params.get(param))
459
+ return f"* {head}: {note}" if note else f"* {head}"
460
+
461
+
462
+ def markdown_params(entry):
463
+ try:
464
+ found = parameters(entry)
465
+ except BuildError as exc:
466
+ return [f"Params: not readable: {prose(exc)}"]
467
+ if not found:
468
+ return ["Params: none."]
469
+ return ["Params:", "", *[markdown_param(entry, *item) for item in found]]
470
+
471
+
472
+ def markdown_examples(entry):
473
+ lines = ["Examples:" if len(entry.examples) > 1 else "Example:"]
474
+ for example in entry.examples:
475
+ lines += ["", "```yaml", example_source(example), "```"]
476
+ return lines
477
+
478
+
479
+ def markdown_entry(entry):
480
+ origin = ", from a plugin" if entry.plugin else ""
481
+ lines = [f"### {entry.name}", "", f"Target `{aliases.target_path(entry.target)}`{origin}."]
482
+ note = strategy_note(entry)
483
+ if note:
484
+ lines += ["", note[0].upper() + note[1:] + "."]
485
+ if flat(entry.doc):
486
+ lines += ["", prose(entry.doc)]
487
+ lines += ["", *markdown_params(entry)]
488
+ if entry.examples:
489
+ lines += ["", *markdown_examples(entry)]
490
+ used = used_in(entry.name)
491
+ if used:
492
+ lines += ["", "Used in " + ", ".join(f"`{path}`" for path in used) + "."]
493
+ return lines
494
+
495
+
496
+ def markdown_place(place):
497
+ return " ".join(f"`{word}`" if "." in word or " " not in place else word for word in place.split())
498
+
499
+
500
+ def docs_markdown():
501
+ lines = ["# kalfa legos"]
502
+ for category, members in categories():
503
+ lines += ["", f"## {category}"]
504
+ if PLACES.get(category):
505
+ lines += ["", f"Goes in {markdown_place(PLACES[category])}."]
506
+ for entry in members:
507
+ lines += ["", *markdown_entry(entry)]
508
+ return "\n".join(lines).rstrip()
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+
3
+ from ..core import run as running
4
+
5
+
6
+ def signatures(directory):
7
+ if not directory.is_dir():
8
+ return {}
9
+ found = {}
10
+ for path in directory.rglob("*"):
11
+ if path.is_file() and not path.name.startswith("."):
12
+ stat = path.stat()
13
+ found[path] = (stat.st_ino, stat.st_mtime_ns, stat.st_size)
14
+ return found
15
+
16
+
17
+ def draw(record, only=None, which=None, device=None):
18
+ ctx = running.open_record(record, which=which, device=device)
19
+ return running.draw_plots(ctx, record, only)
20
+
21
+
22
+ def redraw(record, only=None, which=None, device=None):
23
+ directory = Path(record) / "plots"
24
+ before = signatures(directory)
25
+ draw(record, only, which, device)
26
+ after = signatures(directory)
27
+ return [str(path) for path in sorted(after) if before.get(path) != after[path]]
@@ -0,0 +1,64 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from kalfa import summary as summarizing
5
+ from kalfa.commands.report.page import document
6
+ from kalfa.commands.report.sections import (
7
+ DETAILS,
8
+ chosen_records,
9
+ curves_section,
10
+ details_section,
11
+ folds_section,
12
+ heading,
13
+ ranked,
14
+ summary_section,
15
+ sweep_section,
16
+ title_of,
17
+ )
18
+ from kalfa.core.errors import KalfaError
19
+ from kalfa.core.record import atomic
20
+
21
+
22
+ REPORT = "report.html"
23
+ SUFFIXES = (".html", ".htm")
24
+ __all__ = ["DETAILS", "REPORT", "SUFFIXES", "render", "target_of", "write_report"]
25
+
26
+
27
+ def render(summary, points=None, curves=None, stats=True):
28
+ records = ranked(summary)
29
+ chosen, rest = chosen_records(summary, records, points)
30
+ title = title_of(summary)
31
+ sections = [("summary", "Summary"), ("curves", "Curves")]
32
+ if summary.folds:
33
+ sections.append(("folds", "Folds"))
34
+ if summary.sweep is not None:
35
+ sections.append(("sweep", "Sweep"))
36
+ sections.append(("records", "Records"))
37
+ body = [heading(summary, title, sections), summary_section(summary), curves_section(summary, records, curves)]
38
+ if summary.folds:
39
+ body.append(folds_section(summary))
40
+ if summary.sweep is not None:
41
+ body.append(sweep_section(summary))
42
+ body.append(details_section(chosen, rest, stats))
43
+ return document(title, "\n".join(body))
44
+
45
+
46
+ def target_of(paths, out):
47
+ if out is not None:
48
+ target = Path(out)
49
+ if target.is_dir() or target.suffix.lower() not in SUFFIXES:
50
+ return target / REPORT
51
+ return target
52
+ return Path(paths[0]) / REPORT
53
+
54
+
55
+ def write_report(paths, out=None, by=None, mode=None, top=None, points=None):
56
+ paths = [paths] if isinstance(paths, (str, os.PathLike)) else list(paths)
57
+ if not paths:
58
+ raise KalfaError("a report needs at least one record or sweep root")
59
+ found = summarizing.summarize(paths, by=by, mode=mode, top=top)
60
+ text = render(found, points=points)
61
+ target = target_of(paths, out)
62
+ with atomic(target) as temporary:
63
+ temporary.write_text(text, encoding="utf-8")
64
+ return target