labnotes 0.0.66__tar.gz → 0.0.68__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 (113) hide show
  1. {labnotes-0.0.66 → labnotes-0.0.68}/PKG-INFO +1 -1
  2. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/create_app.py +40 -10
  3. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/create_note.py +2 -2
  4. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/experiment_notes.py +2 -2
  5. labnotes-0.0.68/src/labnotes/note_path.py +16 -0
  6. labnotes-0.0.68/src/labnotes/static/new-note.js +43 -0
  7. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/note-edit.js +18 -12
  8. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/note-editor.js +7 -1
  9. labnotes-0.0.68/src/labnotes/tables/note_text.py +10 -0
  10. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/open_tables.py +1 -1
  11. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/read_file.py +2 -0
  12. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/templates/experiment.html +6 -11
  13. labnotes-0.0.68/src/labnotes/templates/note.html +6 -0
  14. labnotes-0.0.68/src/labnotes/wait_for_note.py +20 -0
  15. labnotes-0.0.66/src/labnotes/static/new-note.js +0 -12
  16. labnotes-0.0.66/src/labnotes/wait_for_note.py +0 -13
  17. {labnotes-0.0.66 → labnotes-0.0.68}/.gitignore +0 -0
  18. {labnotes-0.0.66 → labnotes-0.0.68}/CHANGELOG.md +0 -0
  19. {labnotes-0.0.66 → labnotes-0.0.68}/MIGRATIONS.md +0 -0
  20. {labnotes-0.0.66 → labnotes-0.0.68}/README.md +0 -0
  21. {labnotes-0.0.66 → labnotes-0.0.68}/pyproject.toml +0 -0
  22. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/__init__.py +0 -0
  23. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/asset_url.py +0 -0
  24. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/check_counts.py +0 -0
  25. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/cli.py +0 -0
  26. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/context.md +0 -0
  27. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/context_guide.py +0 -0
  28. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/count_runs.py +0 -0
  29. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/create_experiment.py +0 -0
  30. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/data_html.py +0 -0
  31. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/default_config_text.py +0 -0
  32. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/default_counts.py +0 -0
  33. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/drain_watch.py +0 -0
  34. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/experiment_runs.py +0 -0
  35. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/experiments.py +0 -0
  36. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/file_changes.py +0 -0
  37. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/format_duration.py +0 -0
  38. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/format_value.py +0 -0
  39. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/group_runs.py +0 -0
  40. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/init.py +0 -0
  41. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/invocation_started.py +0 -0
  42. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/is_positive_int.py +0 -0
  43. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/list_experiments.py +0 -0
  44. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/list_runs.py +0 -0
  45. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/load_config.py +0 -0
  46. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/load_experiment.py +0 -0
  47. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/models.py +0 -0
  48. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/modes.py +0 -0
  49. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/nests_runs.py +0 -0
  50. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/notebook_kernel.py +0 -0
  51. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/output_html.py +0 -0
  52. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/parse_run_view.py +0 -0
  53. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/print_rows.py +0 -0
  54. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/python_blocks.py +0 -0
  55. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/read_config.py +0 -0
  56. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/read_path.py +0 -0
  57. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/render_markdown.py +0 -0
  58. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/__init__.py +0 -0
  59. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/describe_event.py +0 -0
  60. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/execute_run.py +0 -0
  61. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/hold_folder.py +0 -0
  62. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/new_param_run_id.py +0 -0
  63. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/new_run_id.py +0 -0
  64. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  65. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/param_args.py +0 -0
  66. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/param_grid.py +0 -0
  67. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/param_run_name.py +0 -0
  68. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/plan_runs.py +0 -0
  69. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/read_experiment_config.py +0 -0
  70. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/run_experiment.py +0 -0
  71. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/run_folder.py +0 -0
  72. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/run_headless.py +0 -0
  73. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/runs_parent.py +0 -0
  74. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/shell_command.py +0 -0
  75. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/start_experiment.py +0 -0
  76. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/stop_then_kill.py +0 -0
  77. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/wait_for_group.py +0 -0
  78. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/runner/write_run_json.py +0 -0
  79. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/save_conflict_copy.py +0 -0
  80. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/save_note.py +0 -0
  81. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/saved_outputs.py +0 -0
  82. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/serve_app.py +0 -0
  83. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/server.py +0 -0
  84. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/sort_href.py +0 -0
  85. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/sql_quote.py +0 -0
  86. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/experiment.js +0 -0
  87. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/labnotes.css +0 -0
  88. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/row-link.js +0 -0
  89. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/run-cell.js +0 -0
  90. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/static/view-toggle.js +0 -0
  91. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/summarize_config.py +0 -0
  92. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/__init__.py +0 -0
  93. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/experiment_rows.py +0 -0
  94. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/file_prefix.py +0 -0
  95. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/iso_time.py +0 -0
  96. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/list_experiment_runs.py +0 -0
  97. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/path_glob.py +0 -0
  98. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/path_pattern.py +0 -0
  99. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/read_experiment.py +0 -0
  100. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/read_run.py +0 -0
  101. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tables/summarize_experiments.py +0 -0
  102. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/templates/base.html +0 -0
  103. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/templates/editor_imports.html +0 -0
  104. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/templates/index.html +0 -0
  105. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/__init__.py +0 -0
  106. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/dim.py +0 -0
  107. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/format_elapsed.py +0 -0
  108. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/run_app.py +0 -0
  109. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/run_tui.py +0 -0
  110. {labnotes-0.0.66 → labnotes-0.0.68}/src/labnotes/tui/runs_table.py +0 -0
  111. {labnotes-0.0.66 → labnotes-0.0.68}/testing-conventions.toml +0 -0
  112. {labnotes-0.0.66 → labnotes-0.0.68}/tests/integration/conftest.py +0 -0
  113. {labnotes-0.0.66 → labnotes-0.0.68}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.66
3
+ Version: 0.0.68
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>
@@ -28,8 +28,10 @@ from labnotes.format_value import format_value
28
28
  from labnotes.list_experiments import list_experiments
29
29
  from labnotes.load_experiment import load_experiment
30
30
  from labnotes.models import LabnotesConfig
31
+ from labnotes.note_path import note_path
31
32
  from labnotes.notebook_kernel import NotebookKernel
32
33
  from labnotes.python_blocks import python_blocks
34
+ from labnotes.render_markdown import render_markdown
33
35
  from labnotes.save_conflict_copy import save_conflict_copy
34
36
  from labnotes.save_note import save_note
35
37
  from labnotes.saved_outputs import saved_outputs
@@ -100,13 +102,24 @@ def create_app(config: LabnotesConfig) -> Starlette:
100
102
  if request.headers.get("content-type") != "application/json":
101
103
  raise HTTPException(415, "Send JSON")
102
104
  name = request.path_params["name"]
103
- filename = create_note(config, name)
104
- if filename is None:
105
+ path = create_note(config, name)
106
+ if path is None:
105
107
  raise HTTPException(404, "No such experiment")
106
- await wait_for_note(request.app.state.tables, config.root, name, filename)
107
- page = request.url_for("experiment", name=name)
108
- url = page.include_query_params(tab="notebooks", note=filename)
109
- return JSONResponse({"url": f"{url.path}?{url.query}#edit"}, status_code=201)
108
+ content = path.read_text(encoding="utf-8")
109
+ return templates.TemplateResponse(
110
+ request,
111
+ "note.html",
112
+ {
113
+ "e": {"name": name},
114
+ "note": {
115
+ "name": path.name,
116
+ "content": content,
117
+ "html": render_markdown(content, ()),
118
+ "runnable": bool(python_blocks(content)),
119
+ },
120
+ },
121
+ status_code=201,
122
+ )
110
123
 
111
124
  async def get_note(request: Request) -> Response:
112
125
  note = await read_note(request)
@@ -118,23 +131,40 @@ def create_app(config: LabnotesConfig) -> Starlette:
118
131
  media_type="text/event-stream",
119
132
  )
120
133
 
134
+ def note_file(request: Request) -> Path:
135
+ if request.headers.get("content-type") != "application/json":
136
+ raise HTTPException(415, "Send JSON")
137
+ path = note_path(
138
+ config, request.path_params["name"], request.path_params["note"]
139
+ )
140
+ if path is None:
141
+ raise HTTPException(404, "No such note")
142
+ return path
143
+
121
144
  async def put_note(request: Request) -> Response:
122
- note = await find_note(request)
145
+ path = note_file(request)
123
146
  body = await request.json()
124
147
  base, content = body.get("base"), body.get("content")
125
148
  if not isinstance(base, str) or not isinstance(content, str):
126
149
  raise HTTPException(400, "Send base and content as strings")
127
- current = save_note(Path(note["path"]), base, content)
150
+ current = save_note(path, base, content)
128
151
  if current is not None:
129
152
  return JSONResponse({"content": current}, status_code=409)
153
+ await wait_for_note(
154
+ request.app.state.tables,
155
+ config.root,
156
+ request.path_params["name"],
157
+ request.path_params["note"],
158
+ content,
159
+ )
130
160
  return Response(status_code=204)
131
161
 
132
162
  async def post_conflict(request: Request) -> Response:
133
- note = await find_note(request)
163
+ path = note_file(request)
134
164
  content = (await request.json()).get("content")
135
165
  if not isinstance(content, str):
136
166
  raise HTTPException(400, "Send content as a string")
137
- copy = save_conflict_copy(Path(note["path"]), content, datetime.now())
167
+ copy = save_conflict_copy(path, content, datetime.now())
138
168
  return JSONResponse({"name": copy.name}, status_code=201)
139
169
 
140
170
  async def run_block(request: Request) -> Response:
@@ -4,7 +4,7 @@ from labnotes.models import LabnotesConfig
4
4
  from labnotes.tables.file_prefix import file_prefix
5
5
 
6
6
 
7
- def create_note(config: LabnotesConfig, experiment: str) -> str | None:
7
+ def create_note(config: LabnotesConfig, experiment: str) -> Path | None:
8
8
  def folder(template: str) -> Path:
9
9
  return config.root / template.replace("{experiment}", experiment)
10
10
 
@@ -17,7 +17,7 @@ def create_note(config: LabnotesConfig, experiment: str) -> str | None:
17
17
  try:
18
18
  with (notes / filename).open("x", encoding="utf-8") as note:
19
19
  note.write("# Untitled\n")
20
- return filename
20
+ return notes / filename
21
21
  except FileExistsError:
22
22
  n += 1
23
23
  filename = f"Untitled {n}.md"
@@ -12,13 +12,13 @@ async def experiment_notes(
12
12
  if note is not None:
13
13
  where += f" AND filename = {quote_string(note)}"
14
14
  rows = await tables.query(
15
- f"SELECT filename, path FROM files WHERE {where} ORDER BY filename"
15
+ f"SELECT filename, path, content FROM files WHERE {where} ORDER BY filename"
16
16
  )
17
17
  return [
18
18
  {
19
19
  "name": str(row["filename"]),
20
20
  "path": str(root / str(row["path"])),
21
- "content": (root / str(row["path"])).read_text(encoding="utf-8"),
21
+ "content": str(row["content"] or ""),
22
22
  }
23
23
  for row in rows
24
24
  ]
@@ -0,0 +1,16 @@
1
+ from pathlib import Path, PurePosixPath
2
+
3
+ from labnotes.models import LabnotesConfig
4
+ from labnotes.tables.file_prefix import file_prefix
5
+ from labnotes.tables.path_pattern import path_pattern
6
+
7
+
8
+ def note_path(config: LabnotesConfig, experiment: str, note: str) -> Path | None:
9
+ relative = file_prefix(config.files).replace("{experiment}", experiment) + note
10
+ if ".." in PurePosixPath(relative).parts or not note.endswith(".md"):
11
+ return None
12
+ found = path_pattern(config.files).fullmatch(relative)
13
+ if found is None or found["experiment"] != experiment:
14
+ return None
15
+ path = config.root / relative
16
+ return path if path.is_relative_to(config.root) and path.is_file() else None
@@ -0,0 +1,43 @@
1
+ function show(note, button) {
2
+ let tabs = document.querySelector(".tabs.sub");
3
+ if (!tabs) {
4
+ tabs = document.createElement("nav");
5
+ tabs.className = "tabs sub";
6
+ button.closest(".empty").replaceWith(tabs);
7
+ tabs.append(button);
8
+ }
9
+ const search = `?tab=notebooks&note=${encodeURIComponent(note.dataset.note)}`;
10
+ const link = document.createElement("a");
11
+ link.href = search;
12
+ link.textContent = note.dataset.note;
13
+ link.setAttribute("aria-current", "page");
14
+ for (const other of tabs.querySelectorAll("a")) other.removeAttribute("aria-current");
15
+ button.before(link);
16
+ const count = document.querySelector(".tabs:not(.sub) [aria-current] .count");
17
+ count.textContent = Number(count.textContent) + 1;
18
+ const old = document.querySelector(".note");
19
+ if (old) old.replaceWith(note);
20
+ else tabs.after(note);
21
+ history.replaceState(null, "", search);
22
+ }
23
+
24
+ for (const button of document.querySelectorAll(".new-note")) {
25
+ // An open editor closes, and reloads the page, on a mousedown outside it.
26
+ button.addEventListener("mousedown", (event) => event.stopPropagation());
27
+ button.addEventListener("click", async () => {
28
+ button.disabled = true;
29
+ const response = await fetch(button.dataset.url, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: "{}",
33
+ });
34
+ button.disabled = false;
35
+ if (!response.ok) return;
36
+ const holder = document.createElement("template");
37
+ holder.innerHTML = await response.text();
38
+ const note = holder.content.querySelector(".note");
39
+ const { attach } = await import(button.dataset.module);
40
+ show(note, button);
41
+ attach(note, true);
42
+ });
43
+ }
@@ -1,34 +1,31 @@
1
- const note = document.querySelector(".note[data-save-url]");
1
+ let detach = () => {};
2
2
 
3
- if (note) {
3
+ export function attach(note, openNow) {
4
+ detach();
4
5
  const prose = note.querySelector(".prose");
5
6
  const status = note.querySelector(".edit-status");
6
7
  const editor = import(note.dataset.editor);
7
8
  const source = JSON.parse(note.querySelector(".note-source").textContent);
8
9
  let editing = null;
10
+ let attached = true;
9
11
 
10
12
  editor.then(
11
13
  () => prose.classList.add("editable"),
12
- () => {
13
- status.textContent = "Editing needs the network: the editor loads from esm.sh.";
14
- status.hidden = false;
15
- },
14
+ () => {},
16
15
  );
17
16
 
18
17
  const open = async (event) => {
19
18
  if (event.target.closest("a, button, output") || getSelection().toString()) return;
20
19
  const { edit } = await editor;
21
- if (editing) return;
20
+ if (editing || !attached) return;
22
21
  prose.removeEventListener("click", open);
23
22
  editing = edit(note, prose, status);
24
23
  };
25
24
  prose.addEventListener("click", open);
26
- if (location.hash === "#edit") {
27
- history.replaceState(null, "", location.pathname + location.search);
28
- open({ target: prose });
29
- }
25
+ if (openNow) open({ target: prose });
30
26
 
31
- new EventSource(note.dataset.changesUrl).addEventListener("message", (event) => {
27
+ const changes = new EventSource(note.dataset.changesUrl);
28
+ changes.addEventListener("message", (event) => {
32
29
  if (JSON.parse(event.data) !== note.dataset.note) return;
33
30
  if (editing) return editing.pull();
34
31
  // A touch or a duplicate event leaves the text alone, and reloading then
@@ -39,4 +36,13 @@ if (note) {
39
36
  if (text !== source) location.reload();
40
37
  });
41
38
  });
39
+
40
+ detach = () => {
41
+ attached = false;
42
+ changes.close();
43
+ editing?.detach();
44
+ };
42
45
  }
46
+
47
+ const note = document.querySelector(".note[data-save-url]");
48
+ if (note) attach(note, false);
@@ -228,5 +228,11 @@ export function edit(note, prose, status) {
228
228
  }),
229
229
  });
230
230
  view.focus();
231
- return { pull: () => run(pull) };
231
+ return {
232
+ pull: () => run(pull),
233
+ detach: () => {
234
+ document.removeEventListener("mousedown", outside);
235
+ save();
236
+ },
237
+ };
232
238
  }
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def note_text(path: Path) -> str | None:
5
+ if path.suffix != ".md":
6
+ return None
7
+ try:
8
+ return path.read_text(encoding="utf-8")
9
+ except UnicodeDecodeError:
10
+ return None
@@ -29,7 +29,7 @@ CREATE VIEW experiments AS
29
29
  FILES = """
30
30
  CREATE TABLE files (
31
31
  experiment TEXT, filename TEXT, path TEXT PRIMARY KEY, ctime TEXT, mtime TEXT,
32
- mtime_ns INTEGER
32
+ mtime_ns INTEGER, content TEXT
33
33
  )
34
34
  """
35
35
  RUNS = """
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
 
3
3
  from labnotes.tables.file_prefix import file_prefix
4
4
  from labnotes.tables.iso_time import iso_time
5
+ from labnotes.tables.note_text import note_text
5
6
  from labnotes.tables.path_pattern import path_pattern
6
7
 
7
8
 
@@ -20,5 +21,6 @@ def read_file(root: Path, template: str, path: str) -> list[dict[str, object]]:
20
21
  "ctime": iso_time(stat.st_ctime),
21
22
  "mtime": iso_time(stat.st_mtime),
22
23
  "mtime_ns": stat.st_mtime_ns,
24
+ "content": note_text(Path(path)),
23
25
  }
24
26
  ]
@@ -35,21 +35,16 @@
35
35
  {% set note = (e.notes | selectattr('name', 'equalto', chosen) | first) or (e.notes | first) %}
36
36
  {% if note %}
37
37
  <nav class="tabs sub">
38
- {% for each in e.notes %}<a href="?tab=notebooks&amp;note={{ each.name | urlencode }}"{% if each.name == note.name %} aria-current="page"{% endif %}>{{ each.name }}</a>{% endfor %}<button type="button" class="new-note" data-url="{{ url_for('post_note', name=e.name) }}" aria-label="New note" title="New note">+</button>
38
+ {% for each in e.notes %}<a href="?tab=notebooks&amp;note={{ each.name | urlencode }}"{% if each.name == note.name %} aria-current="page"{% endif %}>{{ each.name }}</a>{% endfor %}<button type="button" class="new-note" data-url="{{ url_for('post_note', name=e.name) }}" data-module="{{ asset('note-edit.js') }}" aria-label="New note" title="New note">+</button>
39
39
  </nav>
40
- <article class="note" data-run-url="{{ url_for('run_block', name=e.name, note=note.name) }}" data-restart-url="{{ url_for('restart_kernel', name=e.name, note=note.name) }}" data-save-url="{{ url_for('put_note', name=e.name, note=note.name) }}" data-conflict-url="{{ url_for('post_conflict', name=e.name, note=note.name) }}" data-editor="{{ asset('note-editor.js') }}" data-changes-url="{{ url_for('changes', name=e.name) }}" data-note="{{ note.name }}">
41
- {% if note.runnable %}<header class="note-name"><button type="button" class="restart" title="Stop this note's Python kernel. The next Run starts a fresh one.">Restart</button></header>{% endif %}
42
- <p class="edit-status" role="status" hidden></p>
43
- <div class="prose">{{ note.html | safe }}</div>
44
- <script type="application/json" class="note-source">{{ note.content | tojson }}</script>
45
- </article>
46
- {% include "editor_imports.html" %}
40
+ {% include "note.html" %}
47
41
  <script src="{{ asset('run-cell.js') }}" defer></script>
48
- <script type="module" src="{{ asset('note-edit.js') }}"></script>
49
42
  {% else %}
50
- <p class="empty">No notebooks yet. <button type="button" class="new-note" data-url="{{ url_for('post_note', name=e.name) }}" aria-label="New note" title="New note">+</button></p>
43
+ <p class="empty">No notebooks yet. <button type="button" class="new-note" data-url="{{ url_for('post_note', name=e.name) }}" data-module="{{ asset('note-edit.js') }}" aria-label="New note" title="New note">+</button></p>
51
44
  {% endif %}
52
- <script src="{{ asset('new-note.js') }}" defer></script>
45
+ {% include "editor_imports.html" %}
46
+ <script type="module" src="{{ asset('note-edit.js') }}"></script>
47
+ <script type="module" src="{{ asset('new-note.js') }}"></script>
53
48
  {% else %}
54
49
  <section id="runs">
55
50
  {% set view = e.run_view %}
@@ -0,0 +1,6 @@
1
+ <article class="note" data-run-url="{{ url_for('run_block', name=e.name, note=note.name) }}" data-restart-url="{{ url_for('restart_kernel', name=e.name, note=note.name) }}" data-save-url="{{ url_for('put_note', name=e.name, note=note.name) }}" data-conflict-url="{{ url_for('post_conflict', name=e.name, note=note.name) }}" data-editor="{{ asset('note-editor.js') }}" data-changes-url="{{ url_for('changes', name=e.name) }}" data-note="{{ note.name }}">
2
+ {% if note.runnable %}<header class="note-name"><button type="button" class="restart" title="Stop this note's Python kernel. The next Run starts a fresh one.">Restart</button></header>{% endif %}
3
+ <p class="edit-status" role="status" hidden></p>
4
+ <div class="prose">{{ note.html | safe }}</div>
5
+ <script type="application/json" class="note-source">{{ note.content | tojson }}</script>
6
+ </article>
@@ -0,0 +1,20 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ from dirsql import DirSQL
5
+
6
+ from labnotes.experiment_notes import experiment_notes
7
+
8
+
9
+ async def wait_for_note(
10
+ tables: DirSQL,
11
+ root: Path,
12
+ experiment: str,
13
+ note: str,
14
+ content: str | None = None,
15
+ ) -> None:
16
+ for _ in range(40):
17
+ found = await experiment_notes(tables, root, experiment, note)
18
+ if found and (content is None or found[0]["content"] == content):
19
+ return
20
+ await asyncio.sleep(0.05)
@@ -1,12 +0,0 @@
1
- for (const button of document.querySelectorAll(".new-note")) {
2
- button.addEventListener("click", async () => {
3
- button.disabled = true;
4
- const response = await fetch(button.dataset.url, {
5
- method: "POST",
6
- headers: { "Content-Type": "application/json" },
7
- body: "{}",
8
- });
9
- if (response.ok) location.assign((await response.json()).url);
10
- else button.disabled = false;
11
- });
12
- }
@@ -1,13 +0,0 @@
1
- import asyncio
2
- from pathlib import Path
3
-
4
- from dirsql import DirSQL
5
-
6
- from labnotes.experiment_notes import experiment_notes
7
-
8
-
9
- async def wait_for_note(tables: DirSQL, root: Path, experiment: str, note: str) -> None:
10
- for _ in range(40):
11
- if await experiment_notes(tables, root, experiment, note):
12
- return
13
- await asyncio.sleep(0.05)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes