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
kalfa/board/answers.py ADDED
@@ -0,0 +1,419 @@
1
+ import ast
2
+ import json
3
+ import math
4
+ import numbers
5
+ import sys
6
+ from collections.abc import Mapping
7
+ from datetime import date, datetime, time
8
+ from pathlib import Path, PurePath
9
+
10
+ import numpy
11
+
12
+ from kalfa.commands.report import write_report
13
+ from kalfa.commands.stop import stop_record
14
+ from kalfa.core.errors import KalfaError
15
+ from kalfa.records import compare as comparing
16
+ from kalfa.records import reader, stats
17
+ from kalfa.records import sweep as sweeping
18
+ from kalfa.records.reader import file_stamp, last_lines, record_stamp
19
+
20
+
21
+ TEXT_SUFFIXES = (".yaml", ".yml", ".json", ".jsonl", ".txt", ".md", ".csv", ".py", ".sh", ".sub", ".log")
22
+ IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".svg")
23
+ TEXT_LIMIT = 200_000
24
+ EVENTS_LIMIT = 50
25
+ TEXT_TYPE = "text/plain; charset=utf-8"
26
+ JSON_TYPE = "application/json"
27
+ BINARY_TYPE = "application/octet-stream"
28
+ IMAGE_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
29
+ ".svg": "image/svg+xml"}
30
+ GALLERIES = ("plots/", "samples/")
31
+ STATES = ("done", "running", "failed", "stopped", "stale")
32
+ SAMPLE = 2000
33
+ BINS = 40
34
+ MAX_BINS = stats.MAX_BINS
35
+ SERIES_LIMIT = 2000
36
+ WHERE_NODES = (ast.Expression, ast.BoolOp, ast.UnaryOp, ast.BinOp, ast.Compare, ast.Name, ast.Constant, ast.List,
37
+ ast.Tuple, ast.Load, ast.And, ast.Or, ast.Not, ast.Invert, ast.UAdd, ast.USub, ast.Add, ast.Sub,
38
+ ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow, ast.BitAnd, ast.BitOr, ast.BitXor, ast.Eq,
39
+ ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn, ast.Is, ast.IsNot)
40
+
41
+
42
+ class EscapeError(KalfaError):
43
+ pass
44
+
45
+
46
+ class BadValue(KalfaError):
47
+ pass
48
+
49
+
50
+ def segments(relative):
51
+ text = "" if relative is None else str(relative)
52
+ if text.startswith("/") or "\\" in text or "\x00" in text:
53
+ raise EscapeError(f"{text!r} is not a relative posix path")
54
+ parts = text.split("/")
55
+ if parts[-1] == "":
56
+ parts.pop()
57
+ found = []
58
+ for part in parts:
59
+ if part == "..":
60
+ raise EscapeError(f"{text!r} climbs out of the board root")
61
+ if part == "":
62
+ raise EscapeError(f"{text!r} has an empty segment")
63
+ if part != ".":
64
+ found.append(part)
65
+ return found
66
+
67
+
68
+ def inside(base, relative):
69
+ target = base.joinpath(*segments(relative)).resolve()
70
+ if target != base and base not in target.parents:
71
+ raise EscapeError(f"{relative} lies outside {base}")
72
+ return target
73
+
74
+
75
+ def joined(base, name):
76
+ return name if base in ("", ".") else f"{base}/{name}"
77
+
78
+
79
+ def count_of(value, default, label, minimum=None):
80
+ if value is None or value == "":
81
+ return default
82
+ try:
83
+ found = int(value)
84
+ except (TypeError, ValueError) as error:
85
+ raise BadValue(f"{label} must be an integer, not {value!r}") from error
86
+ if minimum is not None and found < minimum:
87
+ raise BadValue(f"{label} must be at least {minimum}, not {value!r}")
88
+ return found
89
+
90
+
91
+ def checked_where(where):
92
+ if where is None or where == "":
93
+ return None
94
+ text = str(where)
95
+ try:
96
+ tree = ast.parse(text, mode="eval")
97
+ except (SyntaxError, ValueError) as error:
98
+ raise BadValue(f"bad where {text!r}: {error}") from error
99
+ for node in ast.walk(tree):
100
+ if not isinstance(node, WHERE_NODES):
101
+ raise BadValue(f"bad where {text!r}: {type(node).__name__} is not allowed")
102
+ return text
103
+
104
+
105
+ def listed(values):
106
+ return [values] if isinstance(values, str) else list(values or [])
107
+
108
+
109
+ def finite_number(value, label):
110
+ try:
111
+ found = float(value)
112
+ except (TypeError, ValueError):
113
+ raise BadValue(f"{label} must be a number, not {value!r}") from None
114
+ if not math.isfinite(found):
115
+ raise BadValue(f"{label} must be a finite number, not {value!r}")
116
+ return found
117
+
118
+
119
+ def histograms_of(histograms):
120
+ if histograms is None or histograms == "" or histograms == {}:
121
+ return None
122
+ if not isinstance(histograms, Mapping):
123
+ raise BadValue("histograms must map histogram names to bin counts, like {\"residual_hist\": 60}")
124
+ return {str(name): count for name, count in histograms.items()}
125
+
126
+
127
+ def overrides_of(pair, **choice):
128
+ found = {key: value for key, value in choice.items() if value is not None and value != ""}
129
+ if "threshold" in found:
130
+ found["threshold"] = finite_number(found["threshold"], "threshold")
131
+ if not found:
132
+ return None
133
+ if pair is None or pair == "":
134
+ named = ["histograms" if key == "bins" else key for key in found]
135
+ raise BadValue(f"{', '.join(named)} needs pair, the prediction column it applies to")
136
+ return {str(pair): found}
137
+
138
+
139
+ def manifest_in(directory):
140
+ return reader.safe_json(directory, reader.MANIFEST)
141
+
142
+
143
+ def image_bases(directory):
144
+ manifest = manifest_in(directory)
145
+ host = manifest.get("host")
146
+ files = manifest.get("config_files")
147
+ found = [directory]
148
+ if isinstance(host, dict) and isinstance(host.get("cwd"), str) and host["cwd"]:
149
+ found.append(Path(host["cwd"]))
150
+ if isinstance(files, list):
151
+ found.extend(Path(file).parent for file in files if isinstance(file, str) and file)
152
+ return [base for base in found if base.is_absolute()]
153
+
154
+
155
+ def image_file(directory, value):
156
+ path = Path(value).expanduser()
157
+ if path.is_absolute():
158
+ return path
159
+ for base in image_bases(directory):
160
+ if (base / path).is_file():
161
+ return base / path
162
+ return directory / path
163
+
164
+
165
+ def summary_or_note(directory, cache=None, manifest=None):
166
+ try:
167
+ return reader.summary_with(cache, directory, manifest)
168
+ except (OSError, ValueError) as error:
169
+ return {"path": str(directory), "name": Path(directory).name, "status": "unknown", "error": str(error)}
170
+
171
+
172
+ def counts_of(root):
173
+ states = [reader.state_of(manifest_in(point)) for point in sweeping.point_dirs(root)]
174
+ return {"count": len(states), **{state: states.count(state) for state in STATES}}
175
+
176
+
177
+ def gallery(files):
178
+ return [entry["path"] for entry in files
179
+ if entry["path"].startswith(GALLERIES) and entry["path"].lower().endswith(IMAGE_SUFFIXES)]
180
+
181
+
182
+ def clipped(data, size):
183
+ cut = data.find(b"\n")
184
+ if 0 <= cut < len(data) - 1:
185
+ data = data[cut + 1:]
186
+ return f"... {size - len(data)} bytes skipped\n".encode("utf-8") + data
187
+
188
+
189
+ def read_text(path, tail=None):
190
+ if tail is None:
191
+ size = path.stat().st_size
192
+ with path.open("rb") as handle:
193
+ if size > TEXT_LIMIT:
194
+ handle.seek(size - TEXT_LIMIT)
195
+ data = handle.read(TEXT_LIMIT)
196
+ else:
197
+ data = handle.read()
198
+ else:
199
+ data = last_lines(path, tail)
200
+ size = len(data)
201
+ data = data[-TEXT_LIMIT:]
202
+ if len(data) >= size:
203
+ return data, tail is None
204
+ return clipped(data, size), False
205
+
206
+
207
+ class Board:
208
+ def __init__(self, root):
209
+ self.root = Path(root).resolve()
210
+ if not self.root.is_dir():
211
+ raise KalfaError(f"{root} is not a directory")
212
+ self.cache = reader.Cache()
213
+
214
+ def resolve(self, relative):
215
+ directory = inside(self.root, relative)
216
+ if not directory.is_dir():
217
+ raise KalfaError(f"no record directory {relative} under {self.root}")
218
+ return directory
219
+
220
+ def name_of(self, path):
221
+ return Path(path).relative_to(self.root).as_posix()
222
+
223
+ def record_dir(self, relative):
224
+ directory = self.resolve(relative)
225
+ if not reader.is_record(directory):
226
+ raise KalfaError(f"{relative} is not a record directory")
227
+ return directory
228
+
229
+ def records(self):
230
+ found = []
231
+ for entry in reader.records_under(self.root):
232
+ directory = self.root / entry["path"]
233
+ if entry["kind"] == "sweep":
234
+ found.append({"path": entry["path"], "kind": "sweep",
235
+ "sweep": {**entry["manifest"], **counts_of(directory)}})
236
+ else:
237
+ found.append({"path": entry["path"], "kind": "record",
238
+ "summary": summary_or_note(directory, self.cache, entry["manifest"])})
239
+ return {"root": str(self.root), "records": found}
240
+
241
+ def record(self, relative):
242
+ directory = self.record_dir(relative)
243
+ manifest = reader.read_manifest(directory)
244
+ plan = reader.read_plan(directory)
245
+ history = reader.read_history(directory)
246
+ files = reader.files_of(directory)
247
+ return {"path": self.name_of(directory), "summary": reader.summary_of(directory), "plan": plan,
248
+ "progress": reader.progress_from(manifest, history), "best": reader.best_from(plan, history),
249
+ "model_summary": reader.model_summary_of(directory), "data_report": reader.data_report_of(directory),
250
+ "events": reader.events_of(directory, limit=EVENTS_LIMIT), "files": files,
251
+ "config": comparing.config_of(directory), "fields": reader.fields_of(directory),
252
+ "has_predictions": (directory / stats.PREDICTIONS).is_file(), "plots": gallery(files),
253
+ "tables": stats.prediction_tables(directory), "failure": reader.failure_of(directory),
254
+ "marks": reader.rule_turns(history)}
255
+
256
+ def series(self, relative, keys=None, limit=SERIES_LIMIT):
257
+ return reader.series_of(self.resolve(relative), keys or None, count_of(limit, SERIES_LIMIT, "limit"))
258
+
259
+ def updates(self, relative, keys=None, limit=SERIES_LIMIT):
260
+ return reader.updates_of(self.resolve(relative), keys or None, count_of(limit, SERIES_LIMIT, "limit"))
261
+
262
+ def events(self, relative, kinds=None, limit=None):
263
+ return reader.events_of(self.resolve(relative), kinds or None, count_of(limit, None, "limit"))
264
+
265
+ def files(self, relative):
266
+ return reader.files_of(self.resolve(relative))
267
+
268
+ def file(self, relative, name, tail=None):
269
+ directory = self.resolve(relative)
270
+ path = inside(directory, name)
271
+ if not path.is_file():
272
+ raise KalfaError(f"no file {name} in {self.name_of(directory)}")
273
+ suffix = path.suffix.lower()
274
+ if suffix not in TEXT_SUFFIXES:
275
+ return path.read_bytes(), IMAGE_TYPES.get(suffix, BINARY_TYPE)
276
+ data, whole = read_text(path, count_of(tail, None, "tail"))
277
+ return data, JSON_TYPE if suffix == ".json" and whole else TEXT_TYPE
278
+
279
+ def predictions(self, relative, where=None, sample=SAMPLE, bins=BINS, name=stats.PREDICTIONS, pair=None,
280
+ task=None, score=None, positive=None, threshold=None, histograms=None):
281
+ directory = self.resolve(relative)
282
+ path = inside(directory, name or stats.PREDICTIONS)
283
+ if not path.is_file():
284
+ return None
285
+ options = {"where": checked_where(where), "sample": count_of(sample, SAMPLE, "sample", 0),
286
+ "bins": min(count_of(bins, BINS, "bins", 1), MAX_BINS),
287
+ "overrides": overrides_of(pair, task=task, score=score, positive=positive, threshold=threshold,
288
+ bins=histograms_of(histograms))}
289
+ try:
290
+ return stats.prediction_stats(directory, plan=reader.read_plan(directory),
291
+ name=path.relative_to(directory).as_posix(), **options)
292
+ except KalfaError as error:
293
+ raise BadValue(str(error)) from error
294
+
295
+ def image(self, relative, index, name=stats.PREDICTIONS):
296
+ directory = self.resolve(relative)
297
+ path = inside(directory, name or stats.PREDICTIONS)
298
+ table = path.relative_to(directory).as_posix()
299
+ if not path.is_file():
300
+ raise KalfaError(f"no table {table} in {self.name_of(directory)}")
301
+ position = count_of(index, None, "index", 0)
302
+ if position is None:
303
+ raise BadValue("index is required")
304
+ frame = stats.load_table(directory, table)
305
+ if frame is None:
306
+ raise KalfaError(f"{table} cannot be read")
307
+ if position >= len(frame):
308
+ raise BadValue(f"index {position} is past the {len(frame)} rows of {table}")
309
+ value = frame["path"].iloc[position] if "path" in frame.columns else None
310
+ suffix = PurePath(value).suffix.lower() if isinstance(value, str) else ""
311
+ if suffix not in stats.IMAGE_TYPES:
312
+ raise BadValue(f"row {position} of {table} names no image file in its path column")
313
+ found = image_file(directory, value)
314
+ if not found.is_file():
315
+ raise KalfaError(f"no image file {value}")
316
+ return found.read_bytes(), stats.IMAGE_TYPES[suffix]
317
+
318
+ def compare(self, relatives, keys=None):
319
+ relatives = listed(relatives)
320
+ if not relatives:
321
+ raise KalfaError("compare needs at least one record")
322
+ directories = [self.record_dir(relative) for relative in relatives]
323
+ found = comparing.compare(directories, keys or None)
324
+ return {**found, "names": [self.name_of(directory) for directory in directories]}
325
+
326
+ def sweep(self, relative):
327
+ root = self.resolve(relative)
328
+ if not sweeping.is_sweep(root):
329
+ raise KalfaError(f"{relative} is not a sweep root")
330
+ spec = sweeping.read_sweep(root)
331
+ objective = spec.get("objective") if isinstance(spec, dict) else None
332
+ points = sorted(sweeping.sweep_points(root, self.cache), key=sweeping.id_key)
333
+ names = []
334
+ for point in points:
335
+ names.extend(key for key in point["values"] if key not in names)
336
+ return {"root": self.name_of(root), "sweep": spec, "points": points,
337
+ "ranked": [point["id"] for point in sweeping.rank_points(points, objective)],
338
+ "best": sweeping.best_point(points, objective),
339
+ "params": {key: [point["values"].get(key) for point in points] for key in names},
340
+ "objectives": [point["objective"] for point in points], "ids": [point["id"] for point in points],
341
+ "order": sweeping.running_best(points, objective), "space": sweeping.axes_of(points, spec)}
342
+
343
+ def stop(self, relative):
344
+ written = stop_record(self.resolve(relative), by="board")
345
+ return {"paths": [self.name_of(path) for path in written]}
346
+
347
+ def report(self, relatives, out=None):
348
+ directories = [self.resolve(relative) for relative in listed(relatives)]
349
+ target = None if out in (None, "") else inside(self.root, out)
350
+ return str(write_report(directories, out=target))
351
+
352
+ def stamps(self):
353
+ found = {}
354
+ for entry in reader.records_under(self.root):
355
+ directory = self.root / entry["path"]
356
+ if entry["kind"] != "sweep":
357
+ found[entry["path"]] = record_stamp(directory, entry["manifest"])
358
+ continue
359
+ parts = [file_stamp(directory / reader.SWEEP)]
360
+ for point in sweeping.point_dirs(directory):
361
+ stamp = record_stamp(point, manifest_in(point))
362
+ found[joined(entry["path"], point.name)] = stamp
363
+ parts.append(f"{point.name}={stamp}")
364
+ found[entry["path"]] = ";".join(parts)
365
+ return found
366
+
367
+
368
+ def key_of(key):
369
+ if isinstance(key, str):
370
+ return str(key)
371
+ found = jsonable(key)
372
+ return found if isinstance(found, str) else json.dumps(found)
373
+
374
+
375
+ def ordered(values):
376
+ try:
377
+ return sorted(values)
378
+ except TypeError:
379
+ return list(values)
380
+
381
+
382
+ def is_tensor(value):
383
+ torch = sys.modules.get("torch")
384
+ return torch is not None and isinstance(value, torch.Tensor)
385
+
386
+
387
+ def jsonable(value):
388
+ if value is None or isinstance(value, bool):
389
+ return value
390
+ if isinstance(value, str):
391
+ return str(value)
392
+ if isinstance(value, int):
393
+ return int(value)
394
+ if isinstance(value, float):
395
+ return float(value) if math.isfinite(value) else None
396
+ if isinstance(value, Mapping):
397
+ return {key_of(key): jsonable(item) for key, item in value.items()}
398
+ if isinstance(value, (list, tuple)):
399
+ return [jsonable(item) for item in value]
400
+ if isinstance(value, (set, frozenset)):
401
+ return [jsonable(item) for item in ordered(value)]
402
+ if isinstance(value, PurePath):
403
+ return str(value)
404
+ if isinstance(value, (datetime, date, time)):
405
+ return value.isoformat()
406
+ if isinstance(value, numpy.ndarray):
407
+ return jsonable(value.tolist())
408
+ if isinstance(value, numpy.generic):
409
+ return jsonable(value.item())
410
+ if is_tensor(value):
411
+ return jsonable(value.detach().cpu().tolist())
412
+ if isinstance(value, (bytes, bytearray)):
413
+ return bytes(value).decode("utf-8", errors="replace")
414
+ if isinstance(value, numbers.Real):
415
+ return jsonable(float(value))
416
+ tolist = getattr(value, "tolist", None)
417
+ if callable(tolist):
418
+ return jsonable(tolist())
419
+ return str(value)