labnotes 0.0.49__tar.gz → 0.0.51__tar.gz

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 (99) hide show
  1. {labnotes-0.0.49 → labnotes-0.0.51}/PKG-INFO +1 -1
  2. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/create_app.py +2 -0
  3. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/experiment_runs.py +4 -1
  4. labnotes-0.0.51/src/labnotes/format_duration.py +5 -0
  5. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/load_experiment.py +1 -1
  6. labnotes-0.0.51/src/labnotes/static/experiment.js +61 -0
  7. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/templates/experiment.html +1 -1
  8. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/run_app.py +9 -1
  9. labnotes-0.0.49/src/labnotes/static/experiment.js +0 -21
  10. {labnotes-0.0.49 → labnotes-0.0.51}/.gitignore +0 -0
  11. {labnotes-0.0.49 → labnotes-0.0.51}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.49 → labnotes-0.0.51}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.49 → labnotes-0.0.51}/README.md +0 -0
  14. {labnotes-0.0.49 → labnotes-0.0.51}/pyproject.toml +0 -0
  15. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/__init__.py +0 -0
  16. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/cell_progress.py +0 -0
  18. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/check_counts.py +0 -0
  19. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/cli.py +0 -0
  20. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/context.md +0 -0
  21. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/create_experiment.py +0 -0
  24. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/data_html.py +0 -0
  25. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/default_config_text.py +0 -0
  26. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/default_counts.py +0 -0
  27. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/drain_watch.py +0 -0
  28. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/experiment_notes.py +0 -0
  29. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/experiment_progress.py +0 -0
  30. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/experiments.py +0 -0
  31. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/format_value.py +0 -0
  32. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/init.py +0 -0
  33. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/is_positive_int.py +0 -0
  34. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/list_experiments.py +0 -0
  35. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/list_runs.py +0 -0
  36. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/load_config.py +0 -0
  37. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/models.py +0 -0
  38. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/modes.py +0 -0
  39. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/notebook_kernel.py +0 -0
  40. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/output_html.py +0 -0
  41. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/parse_run_view.py +0 -0
  42. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/print_rows.py +0 -0
  43. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/python_blocks.py +0 -0
  44. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/read_config.py +0 -0
  45. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/read_path.py +0 -0
  46. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/render_markdown.py +0 -0
  47. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/__init__.py +0 -0
  48. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/describe_event.py +0 -0
  49. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/execute_run.py +0 -0
  50. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/hold_folder.py +0 -0
  51. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/new_run_id.py +0 -0
  52. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  53. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/param_args.py +0 -0
  54. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/param_grid.py +0 -0
  55. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/plan_runs.py +0 -0
  56. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/read_experiment_config.py +0 -0
  57. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/run_experiment.py +0 -0
  58. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/run_folder.py +0 -0
  59. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/run_headless.py +0 -0
  60. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/runs_parent.py +0 -0
  61. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/shell_command.py +0 -0
  62. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/start_experiment.py +0 -0
  63. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/stop_then_kill.py +0 -0
  64. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/wait_for_group.py +0 -0
  65. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/runner/write_run_json.py +0 -0
  66. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/saved_outputs.py +0 -0
  67. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/serve_app.py +0 -0
  68. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/server.py +0 -0
  69. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/sort_href.py +0 -0
  70. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/sql_quote.py +0 -0
  71. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/static/labnotes.css +0 -0
  72. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/static/row-link.js +0 -0
  73. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/static/run-cell.js +0 -0
  74. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/static/view-toggle.js +0 -0
  75. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/stop_idle_kernels.py +0 -0
  76. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/summarize_config.py +0 -0
  77. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/__init__.py +0 -0
  78. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/experiment_rows.py +0 -0
  79. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/file_prefix.py +0 -0
  80. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/iso_time.py +0 -0
  81. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/list_experiment_runs.py +0 -0
  82. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/open_tables.py +0 -0
  83. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/path_glob.py +0 -0
  84. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/path_pattern.py +0 -0
  85. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/read_experiment.py +0 -0
  86. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/read_file.py +0 -0
  87. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/read_run.py +0 -0
  88. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tables/summarize_experiments.py +0 -0
  89. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/templates/base.html +0 -0
  90. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/templates/index.html +0 -0
  91. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/templates/macros.html +0 -0
  92. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/__init__.py +0 -0
  93. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/dim.py +0 -0
  94. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/format_elapsed.py +0 -0
  95. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/run_tui.py +0 -0
  96. {labnotes-0.0.49 → labnotes-0.0.51}/src/labnotes/tui/runs_table.py +0 -0
  97. {labnotes-0.0.49 → labnotes-0.0.51}/testing-conventions.toml +0 -0
  98. {labnotes-0.0.49 → labnotes-0.0.51}/tests/integration/conftest.py +0 -0
  99. {labnotes-0.0.49 → labnotes-0.0.51}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.49
3
+ Version: 0.0.51
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -14,6 +14,7 @@ from starlette.templating import Jinja2Templates
14
14
  from labnotes.asset_url import asset_url
15
15
  from labnotes.drain_watch import drain_watch
16
16
  from labnotes.experiment_notes import experiment_notes
17
+ from labnotes.format_duration import format_duration
17
18
  from labnotes.format_value import format_value
18
19
  from labnotes.list_experiments import list_experiments
19
20
  from labnotes.load_experiment import load_experiment
@@ -27,6 +28,7 @@ from labnotes.tables.open_tables import open_tables
27
28
  HERE = Path(__file__).parent
28
29
  templates = Jinja2Templates(directory=HERE / "templates")
29
30
  templates.env.filters["value"] = format_value
31
+ templates.env.filters["duration"] = format_duration
30
32
  templates.env.globals["asset"] = asset_url
31
33
 
32
34
 
@@ -1,5 +1,8 @@
1
1
  from labnotes.sql_quote import quote_identifier, quote_string
2
2
 
3
+ STARTED = "strftime('%Y-%m-%d %H:%M:%S', created)"
4
+ DURATION = "CAST(round((julianday(ended) - julianday(created)) * 86400) AS INTEGER)"
5
+
3
6
 
4
7
  def experiment_runs(experiment: str, params: list[str]) -> str:
5
8
  columns = "".join(
@@ -8,6 +11,6 @@ def experiment_runs(experiment: str, params: list[str]) -> str:
8
11
  for key in params
9
12
  )
10
13
  return (
11
- f"(SELECT *{columns} FROM runs"
14
+ f"(SELECT *{columns}, {STARTED} AS started, {DURATION} AS duration FROM runs"
12
15
  f" WHERE experiment = {quote_string(experiment)})"
13
16
  )
@@ -0,0 +1,5 @@
1
+ from labnotes.tui.format_elapsed import format_elapsed
2
+
3
+
4
+ def format_duration(seconds: int | None) -> str:
5
+ return "—" if seconds is None else format_elapsed(seconds)
@@ -42,7 +42,7 @@ async def load_experiment(
42
42
  varies = cast(dict[str, list[object]], summary["varies"])
43
43
  source = experiment_runs(name, list(varies))
44
44
  summary = await check_counts(tables, source, summary)
45
- shown = ["id", *(key for key in varies if key != "id")]
45
+ shown = ["id", *(key for key in varies if key != "id"), "started", "duration"]
46
46
  view = parse_run_view(params, shown)
47
47
  [totals] = await count_runs(tables, source, str(summary["completed_run"]))
48
48
  notes = await experiment_notes(tables, root, name)
@@ -0,0 +1,61 @@
1
+ const summary = document.querySelector(".config-summary");
2
+ const open = document.querySelector(".config-open");
3
+ const dialog = document.querySelector(".config-file");
4
+
5
+ if (summary) {
6
+ const fit = () => {
7
+ open.hidden = summary.scrollWidth <= summary.clientWidth;
8
+ };
9
+ new ResizeObserver(fit).observe(summary);
10
+ open.addEventListener("click", () => dialog.showModal());
11
+ dialog.querySelector(".config-close").addEventListener("click", () => dialog.close());
12
+ }
13
+
14
+ document.addEventListener("click", (event) => {
15
+ const row = event.target.closest(".run-row");
16
+ if (!row || getSelection().toString()) return;
17
+ const record = row.nextElementSibling;
18
+ record.hidden = !record.hidden;
19
+ row.setAttribute("aria-expanded", String(!record.hidden));
20
+ });
21
+
22
+ const filters = document.querySelector("#run-filters");
23
+
24
+ if (filters) {
25
+ const runs = document.querySelector("#runs");
26
+ let pending;
27
+ let latest;
28
+
29
+ const refresh = async () => {
30
+ clearTimeout(pending);
31
+ const params = new URLSearchParams();
32
+ for (const [key, value] of new FormData(filters)) {
33
+ if (value) params.append(key, value);
34
+ }
35
+ const url = new URL(location.href);
36
+ url.search = params;
37
+ latest = url.href;
38
+ const response = await fetch(url);
39
+ if (latest !== url.href) return;
40
+ const fresh = new DOMParser()
41
+ .parseFromString(await response.text(), "text/html")
42
+ .querySelector("#runs");
43
+ runs.querySelector(".narrowed")?.remove();
44
+ const narrowed = fresh.querySelector(".narrowed");
45
+ if (narrowed) runs.prepend(narrowed);
46
+ for (const part of ["thead tr:first-child", "tbody"]) {
47
+ runs.querySelector(part).replaceWith(fresh.querySelector(part));
48
+ }
49
+ history.replaceState(null, "", url);
50
+ };
51
+
52
+ filters.addEventListener("submit", (event) => {
53
+ event.preventDefault();
54
+ refresh();
55
+ });
56
+ document.addEventListener("input", (event) => {
57
+ if (event.target.form !== filters || event.target.type !== "search") return;
58
+ clearTimeout(pending);
59
+ pending = setTimeout(refresh, 250);
60
+ });
61
+ }
@@ -78,7 +78,7 @@
78
78
  {% for run in e.runs_list %}
79
79
  <tr class="run-row" aria-expanded="false">
80
80
  <td><span class="status {{ 'good' if run.labnotes_completed else 'bad' }}" title="{{ 'complete' if run.labnotes_completed else 'not complete' }}">{{ '✓' if run.labnotes_completed else '✕' }}</span></td>
81
- {% for column in e.run_columns %}<td class="{{ 'mono' if loop.first }}">{{ run[column] | value }}</td>{% endfor %}
81
+ {% for column in e.run_columns %}<td class="{{ 'mono' if loop.first }}{{ 'num' if column == 'duration' }}">{{ run[column] | duration if column == 'duration' else run[column] | value }}</td>{% endfor %}
82
82
  </tr>
83
83
  <tr class="record-row" hidden>
84
84
  <td colspan="{{ e.run_columns | length + 1 }}">
@@ -21,6 +21,7 @@ STREAMS = ("stdout", "stderr")
21
21
  PENDING, PASSED, FAILED = "○", "✓", "✗"
22
22
  SPINNER = "|/-\\"
23
23
  SKIPPED, DESELECTED = "–", "⊘"
24
+ VIEWING_ACTIONS = {"leave", "quit", "interrupt", "command_palette"}
24
25
 
25
26
 
26
27
  class RunApp(App[None]):
@@ -206,6 +207,9 @@ class RunApp(App[None]):
206
207
  if run in table.rows:
207
208
  table.update_cell(run, column, cell, update_width=True)
208
209
 
210
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
211
+ return self.viewing is None or action in VIEWING_ACTIONS
212
+
209
213
  def show_concurrency(self, event: dict[str, object]) -> None:
210
214
  self.concurrency = int(event["concurrency"]) # type: ignore[call-overload]
211
215
  self.sub_title = f"{self.concurrency} at a time"
@@ -290,7 +294,9 @@ class RunApp(App[None]):
290
294
  self.query_one(Log).clear()
291
295
  self.query_one(DataTable).display = False
292
296
  self.query_one(Log).display = True
293
- self.show(f"{run} stdout and stderr. Escape goes back.")
297
+ self.sub_title = run
298
+ self.show("")
299
+ self.refresh_bindings()
294
300
  self.follow()
295
301
 
296
302
  def follow(self) -> None:
@@ -319,6 +325,8 @@ class RunApp(App[None]):
319
325
  self.viewing = None
320
326
  self.query_one(Log).display = False
321
327
  self.query_one(DataTable).display = True
328
+ self.sub_title = f"{self.concurrency} at a time"
329
+ self.refresh_bindings()
322
330
  if self.idle:
323
331
  self.show_summary()
324
332
  else:
@@ -1,21 +0,0 @@
1
- const summary = document.querySelector(".config-summary");
2
- const open = document.querySelector(".config-open");
3
- const dialog = document.querySelector(".config-file");
4
-
5
- if (summary) {
6
- const fit = () => {
7
- open.hidden = summary.scrollWidth <= summary.clientWidth;
8
- };
9
- new ResizeObserver(fit).observe(summary);
10
- open.addEventListener("click", () => dialog.showModal());
11
- dialog.querySelector(".config-close").addEventListener("click", () => dialog.close());
12
- }
13
-
14
- for (const row of document.querySelectorAll(".run-row")) {
15
- row.addEventListener("click", () => {
16
- if (getSelection().toString()) return;
17
- const record = row.nextElementSibling;
18
- record.hidden = !record.hidden;
19
- row.setAttribute("aria-expanded", String(!record.hidden));
20
- });
21
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes