labnotes 0.0.60__tar.gz → 0.0.62__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.
- {labnotes-0.0.60 → labnotes-0.0.62}/PKG-INFO +1 -1
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/create_app.py +18 -8
- labnotes-0.0.62/src/labnotes/create_note.py +23 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/notebook_kernel.py +18 -27
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/serve_app.py +1 -4
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/server.py +3 -16
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/labnotes.css +2 -0
- labnotes-0.0.62/src/labnotes/static/new-note.js +12 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/note-edit.js +4 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/templates/experiment.html +3 -2
- labnotes-0.0.62/src/labnotes/wait_for_note.py +13 -0
- labnotes-0.0.60/src/labnotes/stop_idle_kernels.py +0 -13
- {labnotes-0.0.60 → labnotes-0.0.62}/.gitignore +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/CHANGELOG.md +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/MIGRATIONS.md +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/README.md +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/pyproject.toml +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/context.md +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/init.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/models.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/testing-conventions.toml +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.60 → labnotes-0.0.62}/uv.lock +0 -0
|
@@ -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
|
|
@@ -30,8 +31,8 @@ from labnotes.notebook_kernel import NotebookKernel
|
|
|
30
31
|
from labnotes.python_blocks import python_blocks
|
|
31
32
|
from labnotes.save_note import save_note
|
|
32
33
|
from labnotes.saved_outputs import saved_outputs
|
|
33
|
-
from labnotes.stop_idle_kernels import stop_idle_kernels
|
|
34
34
|
from labnotes.tables.open_tables import open_tables
|
|
35
|
+
from labnotes.wait_for_note import wait_for_note
|
|
35
36
|
|
|
36
37
|
HERE = Path(__file__).parent
|
|
37
38
|
templates = Jinja2Templates(directory=HERE / "templates")
|
|
@@ -40,20 +41,16 @@ templates.env.filters["duration"] = format_duration
|
|
|
40
41
|
templates.env.globals["asset"] = asset_url
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def create_app(config: LabnotesConfig
|
|
44
|
+
def create_app(config: LabnotesConfig) -> Starlette:
|
|
44
45
|
kernels: dict[tuple[str, str], NotebookKernel] = {}
|
|
45
46
|
|
|
46
47
|
@asynccontextmanager
|
|
47
48
|
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
|
48
49
|
app.state.tables = open_tables(config)
|
|
49
50
|
await app.state.tables.ready()
|
|
50
|
-
|
|
51
|
-
asyncio.create_task(drain_watch(app.state.tables)),
|
|
52
|
-
asyncio.create_task(stop_idle_kernels(kernels, kernel_idle_minutes)),
|
|
53
|
-
]
|
|
51
|
+
watcher = asyncio.create_task(drain_watch(app.state.tables))
|
|
54
52
|
yield
|
|
55
|
-
|
|
56
|
-
watcher.cancel()
|
|
53
|
+
watcher.cancel()
|
|
57
54
|
for kernel in kernels.values():
|
|
58
55
|
await kernel.stop()
|
|
59
56
|
|
|
@@ -97,6 +94,18 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
97
94
|
raise HTTPException(415, "Send JSON")
|
|
98
95
|
return await read_note(request)
|
|
99
96
|
|
|
97
|
+
async def post_note(request: Request) -> Response:
|
|
98
|
+
if request.headers.get("content-type") != "application/json":
|
|
99
|
+
raise HTTPException(415, "Send JSON")
|
|
100
|
+
name = request.path_params["name"]
|
|
101
|
+
filename = create_note(config, name)
|
|
102
|
+
if filename is None:
|
|
103
|
+
raise HTTPException(404, "No such experiment")
|
|
104
|
+
await wait_for_note(request.app.state.tables, config.root, name, filename)
|
|
105
|
+
page = request.url_for("experiment", name=name)
|
|
106
|
+
url = page.include_query_params(tab="notebooks", note=filename)
|
|
107
|
+
return JSONResponse({"url": f"{url.path}?{url.query}#edit"}, status_code=201)
|
|
108
|
+
|
|
100
109
|
async def get_note(request: Request) -> Response:
|
|
101
110
|
note = await read_note(request)
|
|
102
111
|
return PlainTextResponse(note["content"], media_type="text/markdown")
|
|
@@ -140,6 +149,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
140
149
|
Route("/", index),
|
|
141
150
|
Route("/experiments/{name}", experiment),
|
|
142
151
|
Route("/experiments/{name}/changes", changes),
|
|
152
|
+
Route("/experiments/{name}/notes", post_note, methods=["POST"]),
|
|
143
153
|
Route("/experiments/{name}/notes/{note:path}", get_note),
|
|
144
154
|
Route("/experiments/{name}/notes/{note:path}", put_note, methods=["PUT"]),
|
|
145
155
|
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"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import hashlib
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from time import monotonic
|
|
5
4
|
|
|
6
5
|
from jupyter_client.asynchronous.client import AsyncKernelClient
|
|
7
6
|
from jupyter_client.manager import AsyncKernelManager
|
|
@@ -24,7 +23,6 @@ class NotebookKernel:
|
|
|
24
23
|
self._lock = asyncio.Lock()
|
|
25
24
|
self._manager: AsyncKernelManager | None = None
|
|
26
25
|
self._client: AsyncKernelClient | None = None
|
|
27
|
-
self.last_used = monotonic()
|
|
28
26
|
|
|
29
27
|
async def _start(self) -> None:
|
|
30
28
|
self._manager = AsyncKernelManager(kernel_name="python3")
|
|
@@ -35,27 +33,24 @@ class NotebookKernel:
|
|
|
35
33
|
|
|
36
34
|
async def run(self, code: str) -> str:
|
|
37
35
|
async with self._lock:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return html
|
|
57
|
-
finally:
|
|
58
|
-
self.last_used = monotonic()
|
|
36
|
+
if self._client is None:
|
|
37
|
+
await self._start()
|
|
38
|
+
assert self._client is not None
|
|
39
|
+
messages: list[dict[str, object]] = []
|
|
40
|
+
await self._client.execute_interactive(
|
|
41
|
+
code,
|
|
42
|
+
output_hook=lambda message: messages.append(message),
|
|
43
|
+
allow_stdin=False,
|
|
44
|
+
)
|
|
45
|
+
html = output_html(
|
|
46
|
+
[
|
|
47
|
+
{"msg_type": message["msg_type"], "content": message["content"]}
|
|
48
|
+
for message in messages
|
|
49
|
+
if message["msg_type"] not in QUIET
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
self.outputs[block_key(code)] = html
|
|
53
|
+
return html
|
|
59
54
|
|
|
60
55
|
async def stop(self) -> None:
|
|
61
56
|
client, manager = self._client, self._manager
|
|
@@ -68,7 +63,3 @@ class NotebookKernel:
|
|
|
68
63
|
async def restart(self) -> None:
|
|
69
64
|
async with self._lock:
|
|
70
65
|
await self.stop()
|
|
71
|
-
|
|
72
|
-
async def stop_if_idle(self, idle_seconds: float) -> None:
|
|
73
|
-
if not self._lock.locked() and monotonic() - self.last_used > idle_seconds:
|
|
74
|
-
await self.stop()
|
|
@@ -7,10 +7,7 @@ from labnotes.create_app import create_app
|
|
|
7
7
|
from labnotes.read_config import read_config
|
|
8
8
|
|
|
9
9
|
CONFIG = "LABNOTES_CONFIG"
|
|
10
|
-
KERNEL_IDLE_MINUTES = "LABNOTES_KERNEL_IDLE_MINUTES"
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
def serve_app() -> Starlette:
|
|
14
|
-
return create_app(
|
|
15
|
-
read_config(Path(os.environ[CONFIG])), int(os.environ[KERNEL_IDLE_MINUTES])
|
|
16
|
-
)
|
|
13
|
+
return create_app(read_config(Path(os.environ[CONFIG])))
|
|
@@ -5,7 +5,7 @@ from uvicorn.main import main as uvicorn_main
|
|
|
5
5
|
|
|
6
6
|
from labnotes.load_config import load_config
|
|
7
7
|
from labnotes.read_config import CONFIG_FILE
|
|
8
|
-
from labnotes.serve_app import CONFIG
|
|
8
|
+
from labnotes.serve_app import CONFIG
|
|
9
9
|
|
|
10
10
|
# labnotes owns these; the rest of uvicorn's options pass straight through.
|
|
11
11
|
OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
@@ -14,14 +14,6 @@ OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
|
14
14
|
@click.command(
|
|
15
15
|
help="Serve the experiments, their notes and their runs as a local site."
|
|
16
16
|
)
|
|
17
|
-
@click.option(
|
|
18
|
-
"--kernel-idle-minutes",
|
|
19
|
-
type=click.IntRange(min=1),
|
|
20
|
-
default=30,
|
|
21
|
-
show_default=True,
|
|
22
|
-
envvar=KERNEL_IDLE_MINUTES,
|
|
23
|
-
help="Stop a note's Python kernel after this long without a run.",
|
|
24
|
-
)
|
|
25
17
|
@click.option(
|
|
26
18
|
"-h",
|
|
27
19
|
"--host",
|
|
@@ -31,19 +23,14 @@ OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
|
31
23
|
)
|
|
32
24
|
@click.option("-p", "--port", type=int, default=8000, show_default=True)
|
|
33
25
|
@click.pass_context
|
|
34
|
-
def server(
|
|
35
|
-
ctx: click.Context,
|
|
36
|
-
kernel_idle_minutes: int,
|
|
37
|
-
**server_options: object,
|
|
38
|
-
) -> None:
|
|
26
|
+
def server(ctx: click.Context, **server_options: object) -> None:
|
|
39
27
|
config = load_config(ctx)
|
|
40
28
|
if config is None:
|
|
41
29
|
raise click.ClickException(
|
|
42
30
|
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
43
31
|
)
|
|
44
|
-
# Through the environment so --reload and --workers child processes see
|
|
32
|
+
# Through the environment so --reload and --workers child processes see it.
|
|
45
33
|
os.environ[CONFIG] = str(config.path.resolve())
|
|
46
|
-
os.environ[KERNEL_IDLE_MINUTES] = str(kernel_idle_minutes)
|
|
47
34
|
ctx.invoke(
|
|
48
35
|
uvicorn_main,
|
|
49
36
|
app="labnotes.serve_app:serve_app",
|
|
@@ -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&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&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
|
|
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)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from collections.abc import Mapping
|
|
3
|
-
|
|
4
|
-
from labnotes.notebook_kernel import NotebookKernel
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async def stop_idle_kernels(
|
|
8
|
-
kernels: Mapping[tuple[str, str], NotebookKernel], idle_minutes: float
|
|
9
|
-
) -> None:
|
|
10
|
-
while True:
|
|
11
|
-
await asyncio.sleep(60)
|
|
12
|
-
for kernel in list(kernels.values()):
|
|
13
|
-
await kernel.stop_if_idle(idle_minutes * 60)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|