labnotes 0.0.66__tar.gz → 0.0.67__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 (111) hide show
  1. {labnotes-0.0.66 → labnotes-0.0.67}/PKG-INFO +1 -1
  2. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/create_app.py +33 -11
  3. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/create_note.py +2 -2
  4. labnotes-0.0.67/src/labnotes/note_path.py +16 -0
  5. labnotes-0.0.67/src/labnotes/static/new-note.js +43 -0
  6. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/note-edit.js +17 -8
  7. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/note-editor.js +7 -1
  8. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/templates/experiment.html +6 -11
  9. labnotes-0.0.67/src/labnotes/templates/note.html +6 -0
  10. labnotes-0.0.66/src/labnotes/static/new-note.js +0 -12
  11. labnotes-0.0.66/src/labnotes/wait_for_note.py +0 -13
  12. {labnotes-0.0.66 → labnotes-0.0.67}/.gitignore +0 -0
  13. {labnotes-0.0.66 → labnotes-0.0.67}/CHANGELOG.md +0 -0
  14. {labnotes-0.0.66 → labnotes-0.0.67}/MIGRATIONS.md +0 -0
  15. {labnotes-0.0.66 → labnotes-0.0.67}/README.md +0 -0
  16. {labnotes-0.0.66 → labnotes-0.0.67}/pyproject.toml +0 -0
  17. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/__init__.py +0 -0
  18. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/asset_url.py +0 -0
  19. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/check_counts.py +0 -0
  20. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/cli.py +0 -0
  21. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/context.md +0 -0
  22. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/context_guide.py +0 -0
  23. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/count_runs.py +0 -0
  24. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/create_experiment.py +0 -0
  25. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/data_html.py +0 -0
  26. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/default_config_text.py +0 -0
  27. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/default_counts.py +0 -0
  28. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/drain_watch.py +0 -0
  29. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/experiment_notes.py +0 -0
  30. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/experiment_runs.py +0 -0
  31. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/experiments.py +0 -0
  32. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/file_changes.py +0 -0
  33. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/format_duration.py +0 -0
  34. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/format_value.py +0 -0
  35. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/group_runs.py +0 -0
  36. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/init.py +0 -0
  37. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/invocation_started.py +0 -0
  38. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/is_positive_int.py +0 -0
  39. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/list_experiments.py +0 -0
  40. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/list_runs.py +0 -0
  41. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/load_config.py +0 -0
  42. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/load_experiment.py +0 -0
  43. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/models.py +0 -0
  44. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/modes.py +0 -0
  45. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/nests_runs.py +0 -0
  46. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/notebook_kernel.py +0 -0
  47. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/output_html.py +0 -0
  48. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/parse_run_view.py +0 -0
  49. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/print_rows.py +0 -0
  50. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/python_blocks.py +0 -0
  51. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/read_config.py +0 -0
  52. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/read_path.py +0 -0
  53. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/render_markdown.py +0 -0
  54. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/__init__.py +0 -0
  55. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/describe_event.py +0 -0
  56. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/execute_run.py +0 -0
  57. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/hold_folder.py +0 -0
  58. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/new_param_run_id.py +0 -0
  59. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/new_run_id.py +0 -0
  60. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  61. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/param_args.py +0 -0
  62. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/param_grid.py +0 -0
  63. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/param_run_name.py +0 -0
  64. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/plan_runs.py +0 -0
  65. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/read_experiment_config.py +0 -0
  66. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/run_experiment.py +0 -0
  67. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/run_folder.py +0 -0
  68. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/run_headless.py +0 -0
  69. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/runs_parent.py +0 -0
  70. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/shell_command.py +0 -0
  71. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/start_experiment.py +0 -0
  72. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/stop_then_kill.py +0 -0
  73. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/wait_for_group.py +0 -0
  74. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/runner/write_run_json.py +0 -0
  75. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/save_conflict_copy.py +0 -0
  76. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/save_note.py +0 -0
  77. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/saved_outputs.py +0 -0
  78. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/serve_app.py +0 -0
  79. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/server.py +0 -0
  80. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/sort_href.py +0 -0
  81. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/sql_quote.py +0 -0
  82. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/experiment.js +0 -0
  83. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/labnotes.css +0 -0
  84. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/row-link.js +0 -0
  85. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/run-cell.js +0 -0
  86. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/static/view-toggle.js +0 -0
  87. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/summarize_config.py +0 -0
  88. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/__init__.py +0 -0
  89. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/experiment_rows.py +0 -0
  90. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/file_prefix.py +0 -0
  91. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/iso_time.py +0 -0
  92. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/list_experiment_runs.py +0 -0
  93. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/open_tables.py +0 -0
  94. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/path_glob.py +0 -0
  95. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/path_pattern.py +0 -0
  96. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/read_experiment.py +0 -0
  97. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/read_file.py +0 -0
  98. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/read_run.py +0 -0
  99. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tables/summarize_experiments.py +0 -0
  100. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/templates/base.html +0 -0
  101. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/templates/editor_imports.html +0 -0
  102. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/templates/index.html +0 -0
  103. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/__init__.py +0 -0
  104. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/dim.py +0 -0
  105. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/format_elapsed.py +0 -0
  106. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/run_app.py +0 -0
  107. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/run_tui.py +0 -0
  108. {labnotes-0.0.66 → labnotes-0.0.67}/src/labnotes/tui/runs_table.py +0 -0
  109. {labnotes-0.0.66 → labnotes-0.0.67}/testing-conventions.toml +0 -0
  110. {labnotes-0.0.66 → labnotes-0.0.67}/tests/integration/conftest.py +0 -0
  111. {labnotes-0.0.66 → labnotes-0.0.67}/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.67
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,13 +28,14 @@ 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
36
38
  from labnotes.tables.open_tables import open_tables
37
- from labnotes.wait_for_note import wait_for_note
38
39
 
39
40
  HERE = Path(__file__).parent
40
41
  templates = Jinja2Templates(directory=HERE / "templates")
@@ -100,13 +101,24 @@ def create_app(config: LabnotesConfig) -> Starlette:
100
101
  if request.headers.get("content-type") != "application/json":
101
102
  raise HTTPException(415, "Send JSON")
102
103
  name = request.path_params["name"]
103
- filename = create_note(config, name)
104
- if filename is None:
104
+ path = create_note(config, name)
105
+ if path is None:
105
106
  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)
107
+ content = path.read_text(encoding="utf-8")
108
+ return templates.TemplateResponse(
109
+ request,
110
+ "note.html",
111
+ {
112
+ "e": {"name": name},
113
+ "note": {
114
+ "name": path.name,
115
+ "content": content,
116
+ "html": render_markdown(content, ()),
117
+ "runnable": bool(python_blocks(content)),
118
+ },
119
+ },
120
+ status_code=201,
121
+ )
110
122
 
111
123
  async def get_note(request: Request) -> Response:
112
124
  note = await read_note(request)
@@ -118,23 +130,33 @@ def create_app(config: LabnotesConfig) -> Starlette:
118
130
  media_type="text/event-stream",
119
131
  )
120
132
 
133
+ def note_file(request: Request) -> Path:
134
+ if request.headers.get("content-type") != "application/json":
135
+ raise HTTPException(415, "Send JSON")
136
+ path = note_path(
137
+ config, request.path_params["name"], request.path_params["note"]
138
+ )
139
+ if path is None:
140
+ raise HTTPException(404, "No such note")
141
+ return path
142
+
121
143
  async def put_note(request: Request) -> Response:
122
- note = await find_note(request)
144
+ path = note_file(request)
123
145
  body = await request.json()
124
146
  base, content = body.get("base"), body.get("content")
125
147
  if not isinstance(base, str) or not isinstance(content, str):
126
148
  raise HTTPException(400, "Send base and content as strings")
127
- current = save_note(Path(note["path"]), base, content)
149
+ current = save_note(path, base, content)
128
150
  if current is not None:
129
151
  return JSONResponse({"content": current}, status_code=409)
130
152
  return Response(status_code=204)
131
153
 
132
154
  async def post_conflict(request: Request) -> Response:
133
- note = await find_note(request)
155
+ path = note_file(request)
134
156
  content = (await request.json()).get("content")
135
157
  if not isinstance(content, str):
136
158
  raise HTTPException(400, "Send content as a string")
137
- copy = save_conflict_copy(Path(note["path"]), content, datetime.now())
159
+ copy = save_conflict_copy(path, content, datetime.now())
138
160
  return JSONResponse({"name": copy.name}, status_code=201)
139
161
 
140
162
  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"
@@ -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,11 +1,13 @@
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"),
@@ -18,17 +20,15 @@ if (note) {
18
20
  const open = async (event) => {
19
21
  if (event.target.closest("a, button, output") || getSelection().toString()) return;
20
22
  const { edit } = await editor;
21
- if (editing) return;
23
+ if (editing || !attached) return;
22
24
  prose.removeEventListener("click", open);
23
25
  editing = edit(note, prose, status);
24
26
  };
25
27
  prose.addEventListener("click", open);
26
- if (location.hash === "#edit") {
27
- history.replaceState(null, "", location.pathname + location.search);
28
- open({ target: prose });
29
- }
28
+ if (openNow) open({ target: prose });
30
29
 
31
- new EventSource(note.dataset.changesUrl).addEventListener("message", (event) => {
30
+ const changes = new EventSource(note.dataset.changesUrl);
31
+ changes.addEventListener("message", (event) => {
32
32
  if (JSON.parse(event.data) !== note.dataset.note) return;
33
33
  if (editing) return editing.pull();
34
34
  // A touch or a duplicate event leaves the text alone, and reloading then
@@ -39,4 +39,13 @@ if (note) {
39
39
  if (text !== source) location.reload();
40
40
  });
41
41
  });
42
+
43
+ detach = () => {
44
+ attached = false;
45
+ changes.close();
46
+ editing?.detach();
47
+ };
42
48
  }
49
+
50
+ const note = document.querySelector(".note[data-save-url]");
51
+ 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
  }
@@ -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>
@@ -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