labnotes 0.0.32__tar.gz → 0.0.34__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 (94) hide show
  1. {labnotes-0.0.32 → labnotes-0.0.34}/PKG-INFO +1 -2
  2. {labnotes-0.0.32 → labnotes-0.0.34}/pyproject.toml +0 -1
  3. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/context.md +3 -1
  4. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/execute_run.py +2 -0
  5. labnotes-0.0.34/src/labnotes/runner/hold_folder.py +15 -0
  6. labnotes-0.0.34/src/labnotes/runner/new_invocation_id.py +6 -0
  7. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/run_experiment.py +9 -1
  8. labnotes-0.0.34/src/labnotes/runner/runs_parent.py +7 -0
  9. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/start_experiment.py +7 -8
  10. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tui/run_app.py +5 -0
  11. labnotes-0.0.34/testing-conventions.toml +5 -0
  12. {labnotes-0.0.32 → labnotes-0.0.34}/uv.lock +0 -11
  13. labnotes-0.0.32/src/labnotes/runner/lock_path.py +0 -7
  14. {labnotes-0.0.32 → labnotes-0.0.34}/.gitignore +0 -0
  15. {labnotes-0.0.32 → labnotes-0.0.34}/CHANGELOG.md +0 -0
  16. {labnotes-0.0.32 → labnotes-0.0.34}/MIGRATIONS.md +0 -0
  17. {labnotes-0.0.32 → labnotes-0.0.34}/README.md +0 -0
  18. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/__init__.py +0 -0
  19. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/asset_url.py +0 -0
  20. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/cell_progress.py +0 -0
  21. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/check_counts.py +0 -0
  22. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/cli.py +0 -0
  23. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/context_guide.py +0 -0
  24. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/count_runs.py +0 -0
  25. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/create_app.py +0 -0
  26. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/create_experiment.py +0 -0
  27. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/data_html.py +0 -0
  28. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/default_config_text.py +0 -0
  29. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/default_counts.py +0 -0
  30. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/drain_watch.py +0 -0
  31. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/experiment_notes.py +0 -0
  32. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/experiment_progress.py +0 -0
  33. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/experiment_runs.py +0 -0
  34. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/experiments.py +0 -0
  35. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/format_value.py +0 -0
  36. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/init.py +0 -0
  37. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/list_experiments.py +0 -0
  38. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/list_runs.py +0 -0
  39. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/load_config.py +0 -0
  40. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/load_experiment.py +0 -0
  41. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/models.py +0 -0
  42. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/notebook_kernel.py +0 -0
  43. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/output_html.py +0 -0
  44. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/parse_run_view.py +0 -0
  45. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/pivot_cells.py +0 -0
  46. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/print_rows.py +0 -0
  47. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/python_blocks.py +0 -0
  48. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/read_config.py +0 -0
  49. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/read_path.py +0 -0
  50. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/render_markdown.py +0 -0
  51. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/__init__.py +0 -0
  52. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/describe_event.py +0 -0
  53. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/new_run_id.py +0 -0
  54. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/param_args.py +0 -0
  55. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/param_grid.py +0 -0
  56. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/plan_runs.py +0 -0
  57. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/read_experiment_config.py +0 -0
  58. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/run_folder.py +0 -0
  59. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/run_headless.py +0 -0
  60. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/shell_command.py +0 -0
  61. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/stop_then_kill.py +0 -0
  62. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/wait_for_group.py +0 -0
  63. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/runner/write_run_json.py +0 -0
  64. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/saved_outputs.py +0 -0
  65. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/serve_app.py +0 -0
  66. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/server.py +0 -0
  67. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/sort_href.py +0 -0
  68. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/sql_quote.py +0 -0
  69. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/static/labnotes.css +0 -0
  70. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/static/row-link.js +0 -0
  71. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/static/run-cell.js +0 -0
  72. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/static/view-toggle.js +0 -0
  73. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/stop_idle_kernels.py +0 -0
  74. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/summarize_config.py +0 -0
  75. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/__init__.py +0 -0
  76. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/experiment_rows.py +0 -0
  77. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/file_prefix.py +0 -0
  78. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/iso_time.py +0 -0
  79. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/list_experiment_runs.py +0 -0
  80. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/open_tables.py +0 -0
  81. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/path_glob.py +0 -0
  82. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/path_pattern.py +0 -0
  83. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/read_experiment.py +0 -0
  84. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/read_file.py +0 -0
  85. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/read_run.py +0 -0
  86. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tables/summarize_experiments.py +0 -0
  87. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/templates/base.html +0 -0
  88. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/templates/experiment.html +0 -0
  89. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/templates/index.html +0 -0
  90. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/templates/macros.html +0 -0
  91. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tui/__init__.py +0 -0
  92. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tui/dim.py +0 -0
  93. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tui/format_elapsed.py +0 -0
  94. {labnotes-0.0.32 → labnotes-0.0.34}/src/labnotes/tui/run_tui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.32
3
+ Version: 0.0.34
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>
@@ -8,7 +8,6 @@ License-Expression: MIT
8
8
  Requires-Python: >=3.12
9
9
  Requires-Dist: click>=8.5.0
10
10
  Requires-Dist: dirsql>=0.4.110
11
- Requires-Dist: filelock>=4.2.0
12
11
  Requires-Dist: haikunator>=2.1.0
13
12
  Requires-Dist: ipykernel>=7.4.0
14
13
  Requires-Dist: jinja2>=3.1.6
@@ -13,7 +13,6 @@ authors = [{ name = "Kevin Scott", email = "me@thekevinscott.com" }]
13
13
  dependencies = [
14
14
  "click>=8.5.0",
15
15
  "dirsql>=0.4.110",
16
- "filelock>=4.2.0",
17
16
  "haikunator>=2.1.0",
18
17
  "ipykernel>=7.4.0",
19
18
  "jinja2>=3.1.6",
@@ -65,7 +65,9 @@ script so it writes its outputs there.
65
65
  Each param is appended as `--name value`, with the name as written (use
66
66
  `source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
67
67
  and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
68
- code), `stdout.log` and `stderr.log` to the run folder.
68
+ code, and `invocation_id`, shared by every run one `run` command started;
69
+ query it as `run ->> 'invocation_id'`), `stdout.log` and `stderr.log` to the
70
+ run folder.
69
71
  A run counts when it exited 0 and its `run.json` doesn't hold
70
72
  `"excluded": true`, so excluding a run by hand means adding that key; the next
71
73
  `run` replaces it. Other keys you add to `run.json` are kept and show in `run`.
@@ -15,9 +15,11 @@ async def execute_run(
15
15
  params: dict[str, object],
16
16
  running: set[asyncio.subprocess.Process],
17
17
  stopping: asyncio.Event,
18
+ invocation: str,
18
19
  ) -> dict[str, object]:
19
20
  run: dict[str, object] = {
20
21
  "params": params,
22
+ "invocation_id": invocation,
21
23
  "created": iso_time(time.time()),
22
24
  "ended": None,
23
25
  "exit_code": None,
@@ -0,0 +1,15 @@
1
+ import fcntl
2
+ import os
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+
7
+
8
+ @contextmanager
9
+ def hold_folder(folder: Path) -> Iterator[None]:
10
+ descriptor = os.open(folder, os.O_RDONLY)
11
+ try:
12
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
13
+ yield
14
+ finally:
15
+ os.close(descriptor)
@@ -0,0 +1,6 @@
1
+ import hashlib
2
+ import time
3
+
4
+
5
+ def new_invocation_id() -> str:
6
+ return hashlib.sha256(str(time.time_ns()).encode()).hexdigest()[:8]
@@ -12,6 +12,7 @@ from labnotes.models import (
12
12
  Report,
13
13
  )
14
14
  from labnotes.runner.execute_run import execute_run
15
+ from labnotes.runner.new_invocation_id import new_invocation_id
15
16
  from labnotes.runner.new_run_id import new_run_id
16
17
  from labnotes.runner.param_grid import param_grid
17
18
  from labnotes.runner.plan_runs import plan_runs
@@ -39,6 +40,7 @@ async def run_experiment(
39
40
  run in planned or run_folder(config.root, config.runs, name, run).exists()
40
41
  )
41
42
 
43
+ invocation = new_invocation_id()
42
44
  plan: list[Entry] = []
43
45
  for params in plan_runs(
44
46
  param_grid(experiment.params), experiment.repeats, finished
@@ -163,7 +165,13 @@ async def run_experiment(
163
165
  report({"event": "started", "run": run, "params": params})
164
166
  running[run] = set()
165
167
  result = await execute_run(
166
- config.root, folder, experiment.command, params, running[run], stopping
168
+ config.root,
169
+ folder,
170
+ experiment.command,
171
+ params,
172
+ running[run],
173
+ stopping,
174
+ invocation,
167
175
  )
168
176
  del running[run]
169
177
  report({"event": "ended", "run": run, **result})
@@ -0,0 +1,7 @@
1
+ import posixpath
2
+ from pathlib import Path
3
+
4
+
5
+ def runs_parent(root: Path, template: str, experiment: str) -> Path:
6
+ head = template.replace("{experiment}", experiment).split("{run}")[0]
7
+ return root / posixpath.dirname(head)
@@ -2,12 +2,11 @@ from collections.abc import Callable
2
2
  from functools import partial
3
3
  from pathlib import Path
4
4
 
5
- from filelock import FileLock, Timeout
6
-
7
5
  from labnotes.models import LabnotesConfig, Start
8
- from labnotes.runner.lock_path import lock_path
6
+ from labnotes.runner.hold_folder import hold_folder
9
7
  from labnotes.runner.read_experiment_config import read_experiment_config
10
8
  from labnotes.runner.run_experiment import run_experiment
9
+ from labnotes.runner.runs_parent import runs_parent
11
10
 
12
11
 
13
12
  def start_experiment(
@@ -23,11 +22,11 @@ def start_experiment(
23
22
  if not experiment_config.is_file():
24
23
  raise ValueError(f"No experiment.toml for {name} at {experiment_config}.")
25
24
  experiment = read_experiment_config(experiment_config)
26
- lock = lock_path(config.root, config.runs, name)
27
- lock.parent.mkdir(parents=True, exist_ok=True)
25
+ runs = runs_parent(config.root, config.runs, name)
26
+ runs.mkdir(parents=True, exist_ok=True)
28
27
  try:
29
- with FileLock(lock, blocking=False):
28
+ with hold_folder(runs):
30
29
  start = partial(run_experiment, config, name, experiment, concurrency)
31
30
  return drive(start, experiment.name or name)
32
- except Timeout as error:
33
- raise ValueError(f"{name} is already running ({lock} is held).") from error
31
+ except BlockingIOError as error:
32
+ raise ValueError(f"{name} is already running ({runs} is held).") from error
@@ -48,6 +48,7 @@ class RunApp(App[None]):
48
48
  )
49
49
  self.concurrency = 1
50
50
  self.idle = False
51
+ self.next_run: str | None = None
51
52
  self.ended: set[str] = set()
52
53
  self.started: dict[str, float] = {}
53
54
  self.pending: set[str] = set()
@@ -98,6 +99,8 @@ class RunApp(App[None]):
98
99
  self.idle = False
99
100
  self.pending.discard(run)
100
101
  self.started[run] = time.monotonic()
102
+ if run == self.next_run:
103
+ self.show("")
101
104
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
102
105
  cells = (run, args, "running", format_elapsed(0))
103
106
  for column, cell in zip(COLUMNS, cells):
@@ -116,6 +119,7 @@ class RunApp(App[None]):
116
119
  self.reorder(event, table.cursor_row)
117
120
  table.move_cursor(row=table.get_row_index(run))
118
121
  self.show(describe_event(event))
122
+ self.next_run = run
119
123
  case "deselected":
120
124
  self.pending.discard(run)
121
125
  self.set_cell(run, "status", dim("deselected"))
@@ -178,6 +182,7 @@ class RunApp(App[None]):
178
182
  self.set_cell(run, "time", format_elapsed(time.monotonic() - began))
179
183
 
180
184
  def show(self, text: str) -> None:
185
+ self.next_run = None
181
186
  self.query_one("#status", Static).update(text)
182
187
 
183
188
  def action_interrupt(self) -> None:
@@ -0,0 +1,5 @@
1
+ [[python.exempt]]
2
+ path = "src/labnotes/runner/hold_folder.py"
3
+ rules = ["mutation"]
4
+ lines = [12]
5
+ reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
@@ -273,15 +273,6 @@ wheels = [
273
273
  { url = "https://files.pythonhosted.org/packages/4d/dd/8bc67e7d5ffc1d88aa5af511189dfb76dc4e1e5b808fab186146ef2663e5/executing-2.3.0-py3-none-any.whl", hash = "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122", size = 29393, upload-time = "2026-10-10T14:06:02.777Z" },
274
274
  ]
275
275
 
276
- [[package]]
277
- name = "filelock"
278
- version = "4.2.0"
279
- source = { registry = "https://pypi.org/simple" }
280
- sdist = { url = "https://files.pythonhosted.org/packages/f4/a9/1af41b37c3279712b22cdc63aac78a52432202b6fe1f9666a2a3d2831fb4/filelock-4.2.0.tar.gz", hash = "sha256:7a60906c75227cf04d0c273afadc8219400f11aeb13cc69591d4f6cdc6c8036e", size = 569667, upload-time = "2026-10-14T20:57:13.11Z" }
281
- wheels = [
282
- { url = "https://files.pythonhosted.org/packages/8e/a3/9bc26acff301fe1aaea1cc3d82a1d57e0a34df3e1cadbfa91ac2dbcdde5c/filelock-4.2.0-py3-none-any.whl", hash = "sha256:2ff5690882e8cdb00ef31fb3d01a3094c29f30985426c59495afb1733f3b7238", size = 135032, upload-time = "2026-10-14T20:57:11.349Z" },
283
- ]
284
-
285
276
  [[package]]
286
277
  name = "h11"
287
278
  version = "0.16.0"
@@ -473,7 +464,6 @@ source = { editable = "." }
473
464
  dependencies = [
474
465
  { name = "click" },
475
466
  { name = "dirsql" },
476
- { name = "filelock" },
477
467
  { name = "haikunator" },
478
468
  { name = "ipykernel" },
479
469
  { name = "jinja2" },
@@ -497,7 +487,6 @@ dev = [
497
487
  requires-dist = [
498
488
  { name = "click", specifier = ">=8.5.0" },
499
489
  { name = "dirsql", specifier = ">=0.4.110" },
500
- { name = "filelock", specifier = ">=4.2.0" },
501
490
  { name = "haikunator", specifier = ">=2.1.0" },
502
491
  { name = "ipykernel", specifier = ">=7.4.0" },
503
492
  { name = "jinja2", specifier = ">=3.1.6" },
@@ -1,7 +0,0 @@
1
- import posixpath
2
- from pathlib import Path
3
-
4
-
5
- def lock_path(root: Path, template: str, experiment: str) -> Path:
6
- head = template.replace("{experiment}", experiment).split("{run}")[0]
7
- return root / posixpath.dirname(head) / ".labnotes.lock"
File without changes
File without changes
File without changes
File without changes
File without changes