labnotes 0.0.50__tar.gz → 0.0.52__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.50 → labnotes-0.0.52}/PKG-INFO +1 -1
  2. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/create_app.py +18 -5
  3. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/experiment_notes.py +1 -0
  4. labnotes-0.0.52/src/labnotes/save_note.py +9 -0
  5. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/run_app.py +9 -1
  6. {labnotes-0.0.50 → labnotes-0.0.52}/.gitignore +0 -0
  7. {labnotes-0.0.50 → labnotes-0.0.52}/CHANGELOG.md +0 -0
  8. {labnotes-0.0.50 → labnotes-0.0.52}/MIGRATIONS.md +0 -0
  9. {labnotes-0.0.50 → labnotes-0.0.52}/README.md +0 -0
  10. {labnotes-0.0.50 → labnotes-0.0.52}/pyproject.toml +0 -0
  11. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/__init__.py +0 -0
  12. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/asset_url.py +0 -0
  13. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/cell_progress.py +0 -0
  14. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/check_counts.py +0 -0
  15. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/cli.py +0 -0
  16. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/context.md +0 -0
  17. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/context_guide.py +0 -0
  18. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/count_runs.py +0 -0
  19. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/create_experiment.py +0 -0
  20. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/data_html.py +0 -0
  21. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/default_config_text.py +0 -0
  22. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/default_counts.py +0 -0
  23. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/drain_watch.py +0 -0
  24. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/experiment_progress.py +0 -0
  25. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/experiment_runs.py +0 -0
  26. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/experiments.py +0 -0
  27. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/format_duration.py +0 -0
  28. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/format_value.py +0 -0
  29. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/init.py +0 -0
  30. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/is_positive_int.py +0 -0
  31. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/list_experiments.py +0 -0
  32. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/list_runs.py +0 -0
  33. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/load_config.py +0 -0
  34. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/load_experiment.py +0 -0
  35. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/models.py +0 -0
  36. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/modes.py +0 -0
  37. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/notebook_kernel.py +0 -0
  38. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/output_html.py +0 -0
  39. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/parse_run_view.py +0 -0
  40. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/print_rows.py +0 -0
  41. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/python_blocks.py +0 -0
  42. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/read_config.py +0 -0
  43. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/read_path.py +0 -0
  44. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/render_markdown.py +0 -0
  45. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/__init__.py +0 -0
  46. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/describe_event.py +0 -0
  47. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/execute_run.py +0 -0
  48. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/hold_folder.py +0 -0
  49. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/new_run_id.py +0 -0
  50. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  51. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/param_args.py +0 -0
  52. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/param_grid.py +0 -0
  53. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/plan_runs.py +0 -0
  54. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/read_experiment_config.py +0 -0
  55. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/run_experiment.py +0 -0
  56. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/run_folder.py +0 -0
  57. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/run_headless.py +0 -0
  58. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/runs_parent.py +0 -0
  59. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/shell_command.py +0 -0
  60. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/start_experiment.py +0 -0
  61. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/stop_then_kill.py +0 -0
  62. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/wait_for_group.py +0 -0
  63. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/runner/write_run_json.py +0 -0
  64. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/saved_outputs.py +0 -0
  65. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/serve_app.py +0 -0
  66. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/server.py +0 -0
  67. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/sort_href.py +0 -0
  68. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/sql_quote.py +0 -0
  69. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/static/experiment.js +0 -0
  70. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/static/labnotes.css +0 -0
  71. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/static/row-link.js +0 -0
  72. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/static/run-cell.js +0 -0
  73. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/static/view-toggle.js +0 -0
  74. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/stop_idle_kernels.py +0 -0
  75. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/summarize_config.py +0 -0
  76. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/__init__.py +0 -0
  77. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/experiment_rows.py +0 -0
  78. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/file_prefix.py +0 -0
  79. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/iso_time.py +0 -0
  80. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/list_experiment_runs.py +0 -0
  81. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/open_tables.py +0 -0
  82. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/path_glob.py +0 -0
  83. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/path_pattern.py +0 -0
  84. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/read_experiment.py +0 -0
  85. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/read_file.py +0 -0
  86. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/read_run.py +0 -0
  87. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tables/summarize_experiments.py +0 -0
  88. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/templates/base.html +0 -0
  89. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/templates/experiment.html +0 -0
  90. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/templates/index.html +0 -0
  91. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/templates/macros.html +0 -0
  92. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/__init__.py +0 -0
  93. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/dim.py +0 -0
  94. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/format_elapsed.py +0 -0
  95. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/run_tui.py +0 -0
  96. {labnotes-0.0.50 → labnotes-0.0.52}/src/labnotes/tui/runs_table.py +0 -0
  97. {labnotes-0.0.50 → labnotes-0.0.52}/testing-conventions.toml +0 -0
  98. {labnotes-0.0.50 → labnotes-0.0.52}/tests/integration/conftest.py +0 -0
  99. {labnotes-0.0.50 → labnotes-0.0.52}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.50
3
+ Version: 0.0.52
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>
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from starlette.applications import Starlette
7
7
  from starlette.exceptions import HTTPException
8
8
  from starlette.requests import Request
9
- from starlette.responses import HTMLResponse, Response
9
+ from starlette.responses import HTMLResponse, JSONResponse, Response
10
10
  from starlette.routing import Mount, Route
11
11
  from starlette.staticfiles import StaticFiles
12
12
  from starlette.templating import Jinja2Templates
@@ -21,6 +21,7 @@ from labnotes.load_experiment import load_experiment
21
21
  from labnotes.models import LabnotesConfig
22
22
  from labnotes.notebook_kernel import NotebookKernel
23
23
  from labnotes.python_blocks import python_blocks
24
+ from labnotes.save_note import save_note
24
25
  from labnotes.saved_outputs import saved_outputs
25
26
  from labnotes.stop_idle_kernels import stop_idle_kernels
26
27
  from labnotes.tables.open_tables import open_tables
@@ -73,7 +74,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
73
74
  request, "experiment.html", {"experiment": loaded}
74
75
  )
75
76
 
76
- async def note_content(request: Request) -> str:
77
+ async def find_note(request: Request) -> dict[str, str]:
77
78
  if request.headers.get("content-type") != "application/json":
78
79
  raise HTTPException(415, "Send JSON")
79
80
  name = request.path_params["name"]
@@ -84,10 +85,21 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
84
85
  if not found:
85
86
  raise HTTPException(404, "No such note")
86
87
  [note] = found
87
- return note["content"]
88
+ return note
89
+
90
+ async def put_note(request: Request) -> Response:
91
+ note = await find_note(request)
92
+ body = await request.json()
93
+ base, content = body.get("base"), body.get("content")
94
+ if not isinstance(base, str) or not isinstance(content, str):
95
+ raise HTTPException(400, "Send base and content as strings")
96
+ current = save_note(Path(note["path"]), base, content)
97
+ if current is not None:
98
+ return JSONResponse({"content": current}, status_code=409)
99
+ return Response(status_code=204)
88
100
 
89
101
  async def run_block(request: Request) -> Response:
90
- blocks = python_blocks(await note_content(request))
102
+ blocks = python_blocks((await find_note(request))["content"])
91
103
  key = (request.path_params["name"], request.path_params["note"])
92
104
  block = (await request.json()).get("block")
93
105
  if not isinstance(block, int) or not 0 <= block < len(blocks):
@@ -97,7 +109,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
97
109
  return HTMLResponse(await kernels[key].run(blocks[block]))
98
110
 
99
111
  async def restart_kernel(request: Request) -> Response:
100
- await note_content(request)
112
+ await find_note(request)
101
113
  kernel = kernels.get((request.path_params["name"], request.path_params["note"]))
102
114
  if kernel is not None:
103
115
  await kernel.restart()
@@ -107,6 +119,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
107
119
  routes=[
108
120
  Route("/", index),
109
121
  Route("/experiments/{name}", experiment),
122
+ Route("/experiments/{name}/notes/{note:path}", put_note, methods=["PUT"]),
110
123
  Route(
111
124
  "/experiments/{name}/notes/{note:path}/run",
112
125
  run_block,
@@ -17,6 +17,7 @@ async def experiment_notes(
17
17
  return [
18
18
  {
19
19
  "name": str(row["filename"]),
20
+ "path": str(root / str(row["path"])),
20
21
  "content": (root / str(row["path"])).read_text(encoding="utf-8"),
21
22
  }
22
23
  for row in rows
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def save_note(path: Path, base: str, content: str) -> str | None:
5
+ current = path.read_text(encoding="utf-8")
6
+ if current != base:
7
+ return current
8
+ path.write_text(content, encoding="utf-8")
9
+ return None
@@ -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:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes