labnotes 0.0.54__tar.gz → 0.0.55__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 (104) hide show
  1. {labnotes-0.0.54 → labnotes-0.0.55}/PKG-INFO +1 -1
  2. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/list_runs.py +5 -0
  3. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/load_experiment.py +11 -3
  4. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/models.py +6 -1
  5. labnotes-0.0.55/src/labnotes/parse_run_view.py +39 -0
  6. labnotes-0.0.55/src/labnotes/sort_href.py +16 -0
  7. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/experiment.js +12 -0
  8. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/labnotes.css +33 -0
  9. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/templates/experiment.html +5 -2
  10. labnotes-0.0.54/src/labnotes/parse_run_view.py +0 -20
  11. labnotes-0.0.54/src/labnotes/sort_href.py +0 -13
  12. {labnotes-0.0.54 → labnotes-0.0.55}/.gitignore +0 -0
  13. {labnotes-0.0.54 → labnotes-0.0.55}/CHANGELOG.md +0 -0
  14. {labnotes-0.0.54 → labnotes-0.0.55}/MIGRATIONS.md +0 -0
  15. {labnotes-0.0.54 → labnotes-0.0.55}/README.md +0 -0
  16. {labnotes-0.0.54 → labnotes-0.0.55}/pyproject.toml +0 -0
  17. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/__init__.py +0 -0
  18. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/asset_url.py +0 -0
  19. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/cell_progress.py +0 -0
  20. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/check_counts.py +0 -0
  21. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/cli.py +0 -0
  22. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/context.md +0 -0
  23. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/context_guide.py +0 -0
  24. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/count_runs.py +0 -0
  25. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/create_app.py +0 -0
  26. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/create_experiment.py +0 -0
  27. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/data_html.py +0 -0
  28. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/default_config_text.py +0 -0
  29. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/default_counts.py +0 -0
  30. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/drain_watch.py +0 -0
  31. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/experiment_notes.py +0 -0
  32. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/experiment_progress.py +0 -0
  33. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/experiment_runs.py +0 -0
  34. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/experiments.py +0 -0
  35. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/format_duration.py +0 -0
  36. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/format_value.py +0 -0
  37. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/init.py +0 -0
  38. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/is_positive_int.py +0 -0
  39. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/list_experiments.py +0 -0
  40. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/load_config.py +0 -0
  41. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/modes.py +0 -0
  42. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/notebook_kernel.py +0 -0
  43. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/output_html.py +0 -0
  44. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/print_rows.py +0 -0
  45. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/python_blocks.py +0 -0
  46. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/read_config.py +0 -0
  47. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/read_path.py +0 -0
  48. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/render_markdown.py +0 -0
  49. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/__init__.py +0 -0
  50. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/describe_event.py +0 -0
  51. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/execute_run.py +0 -0
  52. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/hold_folder.py +0 -0
  53. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/new_run_id.py +0 -0
  54. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  55. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/param_args.py +0 -0
  56. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/param_grid.py +0 -0
  57. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/plan_runs.py +0 -0
  58. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/read_experiment_config.py +0 -0
  59. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/run_experiment.py +0 -0
  60. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/run_folder.py +0 -0
  61. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/run_headless.py +0 -0
  62. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/runs_parent.py +0 -0
  63. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/shell_command.py +0 -0
  64. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/start_experiment.py +0 -0
  65. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/stop_then_kill.py +0 -0
  66. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/wait_for_group.py +0 -0
  67. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/runner/write_run_json.py +0 -0
  68. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/save_note.py +0 -0
  69. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/saved_outputs.py +0 -0
  70. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/serve_app.py +0 -0
  71. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/server.py +0 -0
  72. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/sql_quote.py +0 -0
  73. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/note-edit.js +0 -0
  74. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/note-editor.js +0 -0
  75. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/row-link.js +0 -0
  76. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/run-cell.js +0 -0
  77. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/static/view-toggle.js +0 -0
  78. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/stop_idle_kernels.py +0 -0
  79. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/summarize_config.py +0 -0
  80. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/__init__.py +0 -0
  81. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/experiment_rows.py +0 -0
  82. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/file_prefix.py +0 -0
  83. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/iso_time.py +0 -0
  84. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/list_experiment_runs.py +0 -0
  85. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/open_tables.py +0 -0
  86. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/path_glob.py +0 -0
  87. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/path_pattern.py +0 -0
  88. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/read_experiment.py +0 -0
  89. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/read_file.py +0 -0
  90. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/read_run.py +0 -0
  91. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tables/summarize_experiments.py +0 -0
  92. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/templates/base.html +0 -0
  93. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/templates/editor_imports.html +0 -0
  94. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/templates/index.html +0 -0
  95. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/templates/macros.html +0 -0
  96. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/__init__.py +0 -0
  97. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/dim.py +0 -0
  98. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/format_elapsed.py +0 -0
  99. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/run_app.py +0 -0
  100. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/run_tui.py +0 -0
  101. {labnotes-0.0.54 → labnotes-0.0.55}/src/labnotes/tui/runs_table.py +0 -0
  102. {labnotes-0.0.54 → labnotes-0.0.55}/testing-conventions.toml +0 -0
  103. {labnotes-0.0.54 → labnotes-0.0.55}/tests/integration/conftest.py +0 -0
  104. {labnotes-0.0.54 → labnotes-0.0.55}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.54
3
+ Version: 0.0.55
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>
@@ -16,6 +16,11 @@ async def list_runs(
16
16
  f" lower({quote_string(value)})) > 0"
17
17
  for column, value in view.filters.items()
18
18
  ]
19
+ where += [
20
+ f"params -> {quote_string(f'$.{quote_identifier(column)}')}"
21
+ f" IN ({', '.join(quote_string(value) for value in picked.values())})"
22
+ for column, picked in view.picked.items()
23
+ ]
19
24
  filtered = f" WHERE {' AND '.join(where)}" if where else ""
20
25
  direction = "DESC" if view.descending else "ASC"
21
26
  if view.sort == "status":
@@ -1,5 +1,5 @@
1
1
  import json
2
- from collections.abc import Callable, Mapping, Sequence
2
+ from collections.abc import Callable, Sequence
3
3
  from pathlib import Path
4
4
  from typing import cast
5
5
 
@@ -9,7 +9,9 @@ from labnotes.check_counts import check_counts
9
9
  from labnotes.count_runs import count_runs
10
10
  from labnotes.experiment_notes import experiment_notes
11
11
  from labnotes.experiment_runs import experiment_runs
12
+ from labnotes.format_value import format_value
12
13
  from labnotes.list_runs import list_runs
14
+ from labnotes.models import QueryParams
13
15
  from labnotes.parse_run_view import parse_run_view
14
16
  from labnotes.python_blocks import python_blocks
15
17
  from labnotes.render_markdown import render_markdown
@@ -27,7 +29,7 @@ async def load_experiment(
27
29
  root: Path,
28
30
  mode: str,
29
31
  name: str,
30
- params: Mapping[str, str],
32
+ params: QueryParams,
31
33
  outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
32
34
  ) -> dict[str, object] | None:
33
35
  found = await tables.query(
@@ -43,7 +45,12 @@ async def load_experiment(
43
45
  source = experiment_runs(name, list(varies))
44
46
  summary = await check_counts(tables, source, summary)
45
47
  shown = ["id", *(key for key in varies if key != "id"), "started", "duration"]
46
- view = parse_run_view(params, shown)
48
+ choices = {
49
+ key: {format_value(value): json.dumps(value) for value in values}
50
+ for key, values in varies.items()
51
+ if key != "id"
52
+ }
53
+ view = parse_run_view(params, shown, choices)
47
54
  [totals] = await count_runs(tables, source, str(summary["completed_run"]))
48
55
  notes = await experiment_notes(tables, root, name)
49
56
  return {
@@ -69,6 +76,7 @@ async def load_experiment(
69
76
  ),
70
77
  "run_columns": shown,
71
78
  "run_view": view,
79
+ "run_choices": choices,
72
80
  "sort_hrefs": {
73
81
  column: sort_href(view, column) for column in ["status", *shown]
74
82
  },
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable, Coroutine
2
2
  from dataclasses import dataclass, field
3
3
  from pathlib import Path
4
- from typing import Any
4
+ from typing import Any, Protocol
5
5
 
6
6
  from labnotes.default_counts import DEFAULT_COUNTS
7
7
  from labnotes.modes import DEFAULT_MODE
@@ -24,12 +24,17 @@ Listen = Callable[[Controls], None]
24
24
  Start = Callable[[Report, Listen], Coroutine[Any, Any, list[dict[str, object]]]]
25
25
 
26
26
 
27
+ class QueryParams(Protocol):
28
+ def getlist(self, key: str) -> list[str]: ...
29
+
30
+
27
31
  @dataclass(frozen=True)
28
32
  class RunView:
29
33
  sort: str | None = None
30
34
  descending: bool = False
31
35
  status: str | None = None
32
36
  filters: dict[str, str] = field(default_factory=dict)
37
+ picked: dict[str, dict[str, str]] = field(default_factory=dict)
33
38
 
34
39
 
35
40
  @dataclass(frozen=True)
@@ -0,0 +1,39 @@
1
+ from collections.abc import Mapping
2
+
3
+ from labnotes.models import QueryParams, RunView
4
+
5
+ STATUSES = {"complete", "incomplete"}
6
+
7
+
8
+ def parse_run_view(
9
+ params: QueryParams,
10
+ columns: list[str],
11
+ choices: Mapping[str, Mapping[str, str]],
12
+ ) -> RunView:
13
+ def last(key: str) -> str | None:
14
+ values = params.getlist(key)
15
+ return values[-1] if values else None
16
+
17
+ sort = last("sort")
18
+ status = last("status")
19
+ return RunView(
20
+ sort=sort if sort in columns or sort == "status" else None,
21
+ descending=last("order") == "desc",
22
+ status=status if status in STATUSES else None,
23
+ filters={
24
+ column: value
25
+ for column in columns
26
+ if column not in choices and (value := (last(f"f.{column}") or "").strip())
27
+ },
28
+ picked={
29
+ column: picked
30
+ for column, values in choices.items()
31
+ if (
32
+ picked := {
33
+ label: values[label]
34
+ for label in params.getlist(f"f.{column}")
35
+ if label in values
36
+ }
37
+ )
38
+ },
39
+ )
@@ -0,0 +1,16 @@
1
+ from urllib.parse import urlencode
2
+
3
+ from labnotes.models import RunView
4
+
5
+
6
+ def sort_href(view: RunView, column: str) -> str:
7
+ params = [("sort", column)]
8
+ if view.sort == column and not view.descending:
9
+ params.append(("order", "desc"))
10
+ if view.status:
11
+ params.append(("status", view.status))
12
+ params += [(f"f.{name}", value) for name, value in view.filters.items()]
13
+ params += [
14
+ (f"f.{name}", label) for name, picked in view.picked.items() for label in picked
15
+ ]
16
+ return f"?{urlencode(params)}#runs"
@@ -46,6 +46,10 @@ if (filters) {
46
46
  for (const part of ["thead tr:first-child", "tbody"]) {
47
47
  runs.querySelector(part).replaceWith(fresh.querySelector(part));
48
48
  }
49
+ const summaries = fresh.querySelectorAll(".pick summary");
50
+ runs.querySelectorAll(".pick summary").forEach((summary, index) => {
51
+ summary.textContent = summaries[index].textContent;
52
+ });
49
53
  history.replaceState(null, "", url);
50
54
  };
51
55
 
@@ -58,4 +62,12 @@ if (filters) {
58
62
  clearTimeout(pending);
59
63
  pending = setTimeout(refresh, 250);
60
64
  });
65
+ document.addEventListener("change", (event) => {
66
+ if (event.target.form === filters && event.target.type === "checkbox") refresh();
67
+ });
68
+ document.addEventListener("click", (event) => {
69
+ for (const pick of runs.querySelectorAll(".pick[open]")) {
70
+ if (!pick.contains(event.target)) pick.open = false;
71
+ }
72
+ });
61
73
  }
@@ -276,6 +276,39 @@ h3 {
276
276
  border-radius: 5px;
277
277
  }
278
278
  .runs-table .filters select { padding: 0.1rem 0; }
279
+ .runs-table .pick { position: relative; }
280
+ .runs-table .pick summary {
281
+ list-style: none;
282
+ cursor: pointer;
283
+ font-size: 0.82rem;
284
+ padding: 0.1rem 0.4rem;
285
+ color: var(--ink);
286
+ background: var(--surface);
287
+ border: 1px solid var(--rule);
288
+ border-radius: 5px;
289
+ white-space: nowrap;
290
+ overflow: hidden;
291
+ text-overflow: ellipsis;
292
+ }
293
+ .runs-table .pick summary::-webkit-details-marker { display: none; }
294
+ .runs-table .pick[open] summary, .runs-table .pick summary:focus-visible { outline: none; border-color: var(--accent); }
295
+ .runs-table .pick .options {
296
+ position: absolute;
297
+ z-index: 1;
298
+ top: calc(100% + 0.25rem);
299
+ left: 0;
300
+ min-width: 100%;
301
+ max-height: 16rem;
302
+ overflow: auto;
303
+ padding: 0.3rem 0;
304
+ background: var(--surface);
305
+ border: 1px solid var(--rule);
306
+ border-radius: 5px;
307
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.08);
308
+ }
309
+ .runs-table .pick label { display: flex; gap: 0.45rem; align-items: center; padding: 0.2rem 0.6rem; font-size: 0.82rem; white-space: nowrap; cursor: pointer; }
310
+ .runs-table .pick label:hover { background: var(--paper); }
311
+ .runs-table .filters .pick input { width: auto; }
279
312
  .runs-table .filters input:focus, .runs-table .filters select:focus { outline: none; border-color: var(--accent); }
280
313
  .runs-table tbody tr:last-child td { border-bottom: 0; }
281
314
  .run-row { cursor: pointer; }
@@ -52,7 +52,7 @@
52
52
  {% else %}
53
53
  <section id="runs">
54
54
  {% set view = e.run_view %}
55
- {% set narrowed = view.status or view.filters %}
55
+ {% set narrowed = view.status or view.filters or view.picked %}
56
56
  {% if narrowed %}<p class="narrowed"><span class="num">{{ e.runs_list | length }}</span> of <span class="num">{{ e.runs }}</span> <a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a></p>{% endif %}
57
57
  {% if e.runs %}
58
58
  <form id="run-filters" method="get" action="#runs">
@@ -75,7 +75,10 @@
75
75
  <option value="complete"{{ ' selected' if view.status == 'complete' }}>✓</option>
76
76
  <option value="incomplete"{{ ' selected' if view.status == 'incomplete' }}>✕</option>
77
77
  </select></td>
78
- {% for column in e.run_columns %}<td><input form="run-filters" type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}"></td>{% endfor %}
78
+ {% for column in e.run_columns %}<td>{% if column in e.run_choices %}{% set picked = view.picked.get(column, {}) %}<details class="pick">
79
+ <summary aria-label="Filter {{ column }}">{{ picked | join(', ') if picked else 'all' }}</summary>
80
+ <div class="options">{% for label in e.run_choices[column] %}<label><input form="run-filters" type="checkbox" name="f.{{ column }}" value="{{ label }}"{{ ' checked' if label in picked }}> {{ label }}</label>{% endfor %}</div>
81
+ </details>{% else %}<input form="run-filters" type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}">{% endif %}</td>{% endfor %}
79
82
  </tr>
80
83
  </thead>
81
84
  <tbody>
@@ -1,20 +0,0 @@
1
- from collections.abc import Mapping
2
-
3
- from labnotes.models import RunView
4
-
5
- STATUSES = {"complete", "incomplete"}
6
-
7
-
8
- def parse_run_view(params: Mapping[str, str], columns: list[str]) -> RunView:
9
- sort = params.get("sort")
10
- status = params.get("status")
11
- return RunView(
12
- sort=sort if sort in columns or sort == "status" else None,
13
- descending=params.get("order") == "desc",
14
- status=status if status in STATUSES else None,
15
- filters={
16
- column: value
17
- for column in columns
18
- if (value := params.get(f"f.{column}", "").strip())
19
- },
20
- )
@@ -1,13 +0,0 @@
1
- from urllib.parse import urlencode
2
-
3
- from labnotes.models import RunView
4
-
5
-
6
- def sort_href(view: RunView, column: str) -> str:
7
- params = {"sort": column}
8
- if view.sort == column and not view.descending:
9
- params["order"] = "desc"
10
- if view.status:
11
- params["status"] = view.status
12
- params.update({f"f.{name}": value for name, value in view.filters.items()})
13
- return f"?{urlencode(params)}#runs"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes