labnotes 0.0.48__tar.gz → 0.0.50__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.48 → labnotes-0.0.50}/PKG-INFO +1 -1
  2. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/create_app.py +2 -0
  3. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/experiment_runs.py +4 -1
  4. labnotes-0.0.50/src/labnotes/format_duration.py +5 -0
  5. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/load_experiment.py +1 -1
  6. labnotes-0.0.50/src/labnotes/static/experiment.js +61 -0
  7. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/static/labnotes.css +10 -9
  8. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/templates/experiment.html +1 -1
  9. labnotes-0.0.48/src/labnotes/static/experiment.js +0 -21
  10. {labnotes-0.0.48 → labnotes-0.0.50}/.gitignore +0 -0
  11. {labnotes-0.0.48 → labnotes-0.0.50}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.48 → labnotes-0.0.50}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.48 → labnotes-0.0.50}/README.md +0 -0
  14. {labnotes-0.0.48 → labnotes-0.0.50}/pyproject.toml +0 -0
  15. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/__init__.py +0 -0
  16. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/cell_progress.py +0 -0
  18. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/check_counts.py +0 -0
  19. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/cli.py +0 -0
  20. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/context.md +0 -0
  21. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/create_experiment.py +0 -0
  24. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/data_html.py +0 -0
  25. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/default_config_text.py +0 -0
  26. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/default_counts.py +0 -0
  27. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/drain_watch.py +0 -0
  28. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/experiment_notes.py +0 -0
  29. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/experiment_progress.py +0 -0
  30. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/experiments.py +0 -0
  31. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/format_value.py +0 -0
  32. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/init.py +0 -0
  33. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/is_positive_int.py +0 -0
  34. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/list_experiments.py +0 -0
  35. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/list_runs.py +0 -0
  36. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/load_config.py +0 -0
  37. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/models.py +0 -0
  38. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/modes.py +0 -0
  39. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/notebook_kernel.py +0 -0
  40. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/output_html.py +0 -0
  41. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/parse_run_view.py +0 -0
  42. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/print_rows.py +0 -0
  43. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/python_blocks.py +0 -0
  44. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/read_config.py +0 -0
  45. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/read_path.py +0 -0
  46. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/render_markdown.py +0 -0
  47. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/__init__.py +0 -0
  48. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/describe_event.py +0 -0
  49. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/execute_run.py +0 -0
  50. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/hold_folder.py +0 -0
  51. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/new_run_id.py +0 -0
  52. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  53. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/param_args.py +0 -0
  54. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/param_grid.py +0 -0
  55. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/plan_runs.py +0 -0
  56. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/read_experiment_config.py +0 -0
  57. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/run_experiment.py +0 -0
  58. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/run_folder.py +0 -0
  59. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/run_headless.py +0 -0
  60. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/runs_parent.py +0 -0
  61. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/shell_command.py +0 -0
  62. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/start_experiment.py +0 -0
  63. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/stop_then_kill.py +0 -0
  64. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/wait_for_group.py +0 -0
  65. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/runner/write_run_json.py +0 -0
  66. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/saved_outputs.py +0 -0
  67. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/serve_app.py +0 -0
  68. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/server.py +0 -0
  69. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/sort_href.py +0 -0
  70. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/sql_quote.py +0 -0
  71. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/static/row-link.js +0 -0
  72. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/static/run-cell.js +0 -0
  73. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/static/view-toggle.js +0 -0
  74. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/stop_idle_kernels.py +0 -0
  75. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/summarize_config.py +0 -0
  76. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/__init__.py +0 -0
  77. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/experiment_rows.py +0 -0
  78. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/file_prefix.py +0 -0
  79. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/iso_time.py +0 -0
  80. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/list_experiment_runs.py +0 -0
  81. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/open_tables.py +0 -0
  82. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/path_glob.py +0 -0
  83. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/path_pattern.py +0 -0
  84. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/read_experiment.py +0 -0
  85. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/read_file.py +0 -0
  86. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/read_run.py +0 -0
  87. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tables/summarize_experiments.py +0 -0
  88. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/templates/base.html +0 -0
  89. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/templates/index.html +0 -0
  90. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/templates/macros.html +0 -0
  91. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/__init__.py +0 -0
  92. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/dim.py +0 -0
  93. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/format_elapsed.py +0 -0
  94. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/run_app.py +0 -0
  95. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/run_tui.py +0 -0
  96. {labnotes-0.0.48 → labnotes-0.0.50}/src/labnotes/tui/runs_table.py +0 -0
  97. {labnotes-0.0.48 → labnotes-0.0.50}/testing-conventions.toml +0 -0
  98. {labnotes-0.0.48 → labnotes-0.0.50}/tests/integration/conftest.py +0 -0
  99. {labnotes-0.0.48 → labnotes-0.0.50}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.48
3
+ Version: 0.0.50
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
+ }
@@ -107,7 +107,7 @@ h3 {
107
107
  margin: 0 0 0.75rem;
108
108
  }
109
109
 
110
- .intro { margin-bottom: 2.5rem; }
110
+ .intro { margin-bottom: 1.5rem; }
111
111
  .lede { color: var(--ink-2); font-size: 1.05rem; margin: 0.6rem 0 0; }
112
112
  .study { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-3); margin: 0.35rem 0 0; }
113
113
  .intro .study { font-size: 0.9rem; margin-top: 0.6rem; }
@@ -189,7 +189,7 @@ h3 {
189
189
  .chip.quiet { background: var(--chip); border-color: var(--rule); }
190
190
 
191
191
  /* experiment.toml line */
192
- .config-line { display: flex; align-items: baseline; gap: 0.75rem; margin-top: 1.1rem; }
192
+ .config-line { display: flex; align-items: baseline; gap: 0.75rem; margin-top: 0.75rem; }
193
193
  .config-summary {
194
194
  flex: 1;
195
195
  min-width: 0;
@@ -237,12 +237,12 @@ h3 {
237
237
  .config-file pre { margin: 0; padding: 1rem 1.25rem; max-height: 70vh; overflow: auto; font-size: 0.82rem; }
238
238
 
239
239
  /* Tabs */
240
- .tabs { display: flex; gap: 1.6rem; border-bottom: 1px solid var(--rule); margin-bottom: 1.75rem; font-size: 0.95rem; }
241
- .tabs a { color: var(--ink-2); text-decoration: none; padding: 0.5rem 0; margin-bottom: -1px; border-bottom: 2px solid transparent; }
240
+ .tabs { display: flex; gap: 1.6rem; border-bottom: 1px solid var(--rule); margin-bottom: 1rem; font-size: 0.95rem; }
241
+ .tabs a { color: var(--ink-2); text-decoration: none; padding: 0.35rem 0; margin-bottom: -1px; border-bottom: 2px solid transparent; }
242
242
  .tabs a:hover { color: var(--ink); }
243
243
  .tabs a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }
244
244
  .tabs .count { font-size: 0.8rem; color: var(--ink-3); margin-left: 0.3rem; }
245
- .tabs.sub { gap: 1.2rem; margin-top: -1rem; font-family: var(--mono); font-size: 0.8rem; }
245
+ .tabs.sub { gap: 1.2rem; margin-top: -0.4rem; font-family: var(--mono); font-size: 0.8rem; }
246
246
 
247
247
  /* Runs table */
248
248
  .narrowed { color: var(--ink-2); font-size: 0.88rem; margin: 0 0 0.75rem; }
@@ -255,26 +255,27 @@ h3 {
255
255
  border: 1px solid var(--rule);
256
256
  border-radius: 10px;
257
257
  font-size: 0.86rem;
258
+ line-height: 1.4;
258
259
  }
259
- .runs-table th, .runs-table td { padding: 0.55rem 1rem; text-align: left; border-bottom: 1px solid var(--rule); }
260
+ .runs-table th, .runs-table td { padding: 0.3rem 0.75rem; text-align: left; border-bottom: 1px solid var(--rule); }
260
261
  .runs-table th { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap; }
261
262
  .runs-table th:first-child, .runs-table .run-row td:first-child { width: 4rem; }
262
263
  .runs-table .sort { color: inherit; text-decoration: none; }
263
264
  .runs-table .sort:hover, .runs-table .sorted { color: var(--ink); }
264
265
  .runs-table .arrow { margin-left: 0.3rem; color: var(--accent); }
265
- .runs-table .filters td { padding-top: 0.4rem; padding-bottom: 0.4rem; background: var(--paper); }
266
+ .runs-table .filters td { padding-top: 0.25rem; padding-bottom: 0.25rem; background: var(--paper); }
266
267
  .runs-table .filters input, .runs-table .filters select {
267
268
  width: 100%;
268
269
  min-width: 0;
269
270
  font: inherit;
270
271
  font-size: 0.82rem;
271
- padding: 0.2rem 0.45rem;
272
+ padding: 0.1rem 0.4rem;
272
273
  color: var(--ink);
273
274
  background: var(--surface);
274
275
  border: 1px solid var(--rule);
275
276
  border-radius: 5px;
276
277
  }
277
- .runs-table .filters select { padding: 0.2rem 0; }
278
+ .runs-table .filters select { padding: 0.1rem 0; }
278
279
  .runs-table .filters input:focus, .runs-table .filters select:focus { outline: none; border-color: var(--accent); }
279
280
  .runs-table tbody tr:last-child td { border-bottom: 0; }
280
281
  .run-row { cursor: pointer; }
@@ -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 }}">
@@ -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