labnotes 0.0.60__tar.gz → 0.0.61__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.60 → labnotes-0.0.61}/PKG-INFO +1 -1
  2. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/create_app.py +15 -0
  3. labnotes-0.0.61/src/labnotes/create_note.py +23 -0
  4. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/labnotes.css +2 -0
  5. labnotes-0.0.61/src/labnotes/static/new-note.js +12 -0
  6. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/note-edit.js +4 -0
  7. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/templates/experiment.html +3 -2
  8. labnotes-0.0.61/src/labnotes/wait_for_note.py +13 -0
  9. {labnotes-0.0.60 → labnotes-0.0.61}/.gitignore +0 -0
  10. {labnotes-0.0.60 → labnotes-0.0.61}/CHANGELOG.md +0 -0
  11. {labnotes-0.0.60 → labnotes-0.0.61}/MIGRATIONS.md +0 -0
  12. {labnotes-0.0.60 → labnotes-0.0.61}/README.md +0 -0
  13. {labnotes-0.0.60 → labnotes-0.0.61}/pyproject.toml +0 -0
  14. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/__init__.py +0 -0
  15. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/asset_url.py +0 -0
  16. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/cell_progress.py +0 -0
  17. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/check_counts.py +0 -0
  18. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/cli.py +0 -0
  19. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/context.md +0 -0
  20. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/context_guide.py +0 -0
  21. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/count_runs.py +0 -0
  22. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/data_html.py +0 -0
  24. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/default_config_text.py +0 -0
  25. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/default_counts.py +0 -0
  26. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/drain_watch.py +0 -0
  27. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/experiment_notes.py +0 -0
  28. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/experiment_progress.py +0 -0
  29. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/experiment_runs.py +0 -0
  30. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/experiments.py +0 -0
  31. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/file_changes.py +0 -0
  32. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/format_duration.py +0 -0
  33. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/format_value.py +0 -0
  34. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/group_runs.py +0 -0
  35. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/init.py +0 -0
  36. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/invocation_started.py +0 -0
  37. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/is_positive_int.py +0 -0
  38. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/list_experiments.py +0 -0
  39. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/list_runs.py +0 -0
  40. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/load_config.py +0 -0
  41. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/load_experiment.py +0 -0
  42. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/models.py +0 -0
  43. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/modes.py +0 -0
  44. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/nests_runs.py +0 -0
  45. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/notebook_kernel.py +0 -0
  46. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/output_html.py +0 -0
  47. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/parse_run_view.py +0 -0
  48. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/print_rows.py +0 -0
  49. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/python_blocks.py +0 -0
  50. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/read_config.py +0 -0
  51. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/read_path.py +0 -0
  52. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/render_markdown.py +0 -0
  53. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/__init__.py +0 -0
  54. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/describe_event.py +0 -0
  55. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/execute_run.py +0 -0
  56. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/hold_folder.py +0 -0
  57. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/new_param_run_id.py +0 -0
  58. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/new_run_id.py +0 -0
  59. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  60. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/param_args.py +0 -0
  61. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/param_grid.py +0 -0
  62. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/param_run_name.py +0 -0
  63. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/plan_runs.py +0 -0
  64. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/read_experiment_config.py +0 -0
  65. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/run_experiment.py +0 -0
  66. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/run_folder.py +0 -0
  67. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/run_headless.py +0 -0
  68. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/runs_parent.py +0 -0
  69. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/shell_command.py +0 -0
  70. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/start_experiment.py +0 -0
  71. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/stop_then_kill.py +0 -0
  72. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/wait_for_group.py +0 -0
  73. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/runner/write_run_json.py +0 -0
  74. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/save_note.py +0 -0
  75. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/saved_outputs.py +0 -0
  76. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/serve_app.py +0 -0
  77. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/server.py +0 -0
  78. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/sort_href.py +0 -0
  79. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/sql_quote.py +0 -0
  80. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/experiment.js +0 -0
  81. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/note-editor.js +0 -0
  82. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/row-link.js +0 -0
  83. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/run-cell.js +0 -0
  84. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/static/view-toggle.js +0 -0
  85. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/stop_idle_kernels.py +0 -0
  86. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/summarize_config.py +0 -0
  87. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/__init__.py +0 -0
  88. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/experiment_rows.py +0 -0
  89. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/file_prefix.py +0 -0
  90. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/iso_time.py +0 -0
  91. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/list_experiment_runs.py +0 -0
  92. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/open_tables.py +0 -0
  93. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/path_glob.py +0 -0
  94. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/path_pattern.py +0 -0
  95. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/read_experiment.py +0 -0
  96. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/read_file.py +0 -0
  97. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/read_run.py +0 -0
  98. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tables/summarize_experiments.py +0 -0
  99. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/templates/base.html +0 -0
  100. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/templates/editor_imports.html +0 -0
  101. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/templates/index.html +0 -0
  102. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/templates/macros.html +0 -0
  103. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/__init__.py +0 -0
  104. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/dim.py +0 -0
  105. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/format_elapsed.py +0 -0
  106. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/run_app.py +0 -0
  107. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/run_tui.py +0 -0
  108. {labnotes-0.0.60 → labnotes-0.0.61}/src/labnotes/tui/runs_table.py +0 -0
  109. {labnotes-0.0.60 → labnotes-0.0.61}/testing-conventions.toml +0 -0
  110. {labnotes-0.0.60 → labnotes-0.0.61}/tests/integration/conftest.py +0 -0
  111. {labnotes-0.0.60 → labnotes-0.0.61}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.60
3
+ Version: 0.0.61
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>
@@ -18,6 +18,7 @@ from starlette.staticfiles import StaticFiles
18
18
  from starlette.templating import Jinja2Templates
19
19
 
20
20
  from labnotes.asset_url import asset_url
21
+ from labnotes.create_note import create_note
21
22
  from labnotes.drain_watch import drain_watch
22
23
  from labnotes.experiment_notes import experiment_notes
23
24
  from labnotes.file_changes import file_changes
@@ -32,6 +33,7 @@ from labnotes.save_note import save_note
32
33
  from labnotes.saved_outputs import saved_outputs
33
34
  from labnotes.stop_idle_kernels import stop_idle_kernels
34
35
  from labnotes.tables.open_tables import open_tables
36
+ from labnotes.wait_for_note import wait_for_note
35
37
 
36
38
  HERE = Path(__file__).parent
37
39
  templates = Jinja2Templates(directory=HERE / "templates")
@@ -97,6 +99,18 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
97
99
  raise HTTPException(415, "Send JSON")
98
100
  return await read_note(request)
99
101
 
102
+ async def post_note(request: Request) -> Response:
103
+ if request.headers.get("content-type") != "application/json":
104
+ raise HTTPException(415, "Send JSON")
105
+ name = request.path_params["name"]
106
+ filename = create_note(config, name)
107
+ if filename is None:
108
+ raise HTTPException(404, "No such experiment")
109
+ await wait_for_note(request.app.state.tables, config.root, name, filename)
110
+ page = request.url_for("experiment", name=name)
111
+ url = page.include_query_params(tab="notebooks", note=filename)
112
+ return JSONResponse({"url": f"{url.path}?{url.query}#edit"}, status_code=201)
113
+
100
114
  async def get_note(request: Request) -> Response:
101
115
  note = await read_note(request)
102
116
  return PlainTextResponse(note["content"], media_type="text/markdown")
@@ -140,6 +154,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
140
154
  Route("/", index),
141
155
  Route("/experiments/{name}", experiment),
142
156
  Route("/experiments/{name}/changes", changes),
157
+ Route("/experiments/{name}/notes", post_note, methods=["POST"]),
143
158
  Route("/experiments/{name}/notes/{note:path}", get_note),
144
159
  Route("/experiments/{name}/notes/{note:path}", put_note, methods=["PUT"]),
145
160
  Route(
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from labnotes.models import LabnotesConfig
4
+ from labnotes.tables.file_prefix import file_prefix
5
+
6
+
7
+ def create_note(config: LabnotesConfig, experiment: str) -> str | None:
8
+ def folder(template: str) -> Path:
9
+ return config.root / template.replace("{experiment}", experiment)
10
+
11
+ if experiment.startswith(".") or not folder(config.experiments).is_dir():
12
+ return None
13
+ notes = folder(file_prefix(config.files))
14
+ notes.mkdir(parents=True, exist_ok=True)
15
+ filename, n = "Untitled.md", 1
16
+ while True:
17
+ try:
18
+ with (notes / filename).open("x", encoding="utf-8") as note:
19
+ note.write("# Untitled\n")
20
+ return filename
21
+ except FileExistsError:
22
+ n += 1
23
+ filename = f"Untitled {n}.md"
@@ -243,6 +243,8 @@ h3 {
243
243
  .tabs a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }
244
244
  .tabs .count { font-size: 0.8rem; color: var(--ink-3); margin-left: 0.3rem; }
245
245
  .tabs.sub { gap: 1.2rem; margin-top: -0.4rem; font-family: var(--mono); font-size: 0.8rem; }
246
+ .new-note { font: inherit; color: var(--ink-2); background: none; border: 0; padding: 0 0.2rem; cursor: pointer; }
247
+ .new-note:hover { color: var(--ink); }
246
248
 
247
249
  /* Runs table */
248
250
  .narrowed { color: var(--ink-2); font-size: 0.88rem; margin: 0 0 0.75rem; }
@@ -0,0 +1,12 @@
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
+ }
@@ -23,6 +23,10 @@ if (note) {
23
23
  editing = edit(note, prose, status);
24
24
  };
25
25
  prose.addEventListener("click", open);
26
+ if (location.hash === "#edit") {
27
+ history.replaceState(null, "", location.pathname + location.search);
28
+ open({ target: prose });
29
+ }
26
30
 
27
31
  new EventSource(note.dataset.changesUrl).addEventListener("message", (event) => {
28
32
  if (JSON.parse(event.data) !== note.dataset.note) return;
@@ -35,7 +35,7 @@
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 %}
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>
39
39
  </nav>
40
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-editor="{{ asset('note-editor.js') }}" data-changes-url="{{ url_for('changes', name=e.name) }}" data-note="{{ note.name }}">
41
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 %}
@@ -47,8 +47,9 @@
47
47
  <script src="{{ asset('run-cell.js') }}" defer></script>
48
48
  <script type="module" src="{{ asset('note-edit.js') }}"></script>
49
49
  {% else %}
50
- <p class="empty">No notebooks yet.</p>
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>
51
51
  {% endif %}
52
+ <script src="{{ asset('new-note.js') }}" defer></script>
52
53
  {% else %}
53
54
  <section id="runs">
54
55
  {% set view = e.run_view %}
@@ -0,0 +1,13 @@
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