/api/notebooks/* — the .ipynb files a notebook session edits (v1).
Thin HTTP over :mod:tit.server.notebooks; every path rule, every name check
and the nbformat read/write live there. One directory,
<project>/code/ti-toolbox/notebooks, which is also where the pipeline
canvas's "Export notebook" can now land a notebook instead of only handing it
to a download.
Import cost is deliberately small (dev/route_import_guard.py): nbformat
is imported inside :mod:tit.server.notebooks' own functions, never here.
create_notebook
Create name.
With no content, the new notebook is the starter: a markdown cell and
a first code cell that already imports tit and prints this project's
root, so "the TI-Toolbox environment is automatically loaded" is something
the author can see rather than be told.
With content, this is the import path — "Import .ipynb" in the UI, and
the pipeline canvas saving its export. Same validation either way.
Source code in tit/server/routes/notebooks.py
| @router.post(
"/api/notebooks",
response_model=Notebook,
responses={409: {"description": "a notebook of that name already exists"}},
summary="Create a notebook, or store one that was uploaded",
)
def create_notebook(request: Request, body: dict[str, Any] = Body(...)) -> dict[str, Any]:
"""Create ``name``.
With no ``content``, the new notebook is the starter: a markdown cell and
a first code cell that already imports ``tit`` and prints this project's
root, so "the TI-Toolbox environment is automatically loaded" is something
the author can see rather than be told.
With ``content``, this is the import path — "Import .ipynb" in the UI, and
the pipeline canvas saving its export. Same validation either way.
"""
root = _project_root(request)
name = body.get("name")
if not isinstance(name, str):
raise HTTPException(status_code=422, detail="'name' is required.")
content = body.get("content")
if content is not None and not isinstance(content, dict):
raise HTTPException(status_code=422, detail="'content' must be a notebook object.")
try:
file_name = nb.normalise_name(name)
path = nb.notebook_path(root, file_name)
if path.exists() and not bool(body.get("overwrite", False)):
raise HTTPException(
status_code=409, detail=f"{file_name} already exists in this project."
)
document = content if content is not None else nb.new_notebook()
nb.write_notebook(root, file_name, document)
return {"name": file_name, "content": nb.read_notebook(root, file_name)}
except nb.NotebookError as error:
raise _fail(error) from error
|