72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""
|
|
A document, before it is any particular kind of file.
|
|
|
|
Three block types and nothing else. A `Doc` does not know what a notebook is,
|
|
does not know what HTML is, and above all does not know what a graph is — the
|
|
emitters know that, and there is one per output format.
|
|
|
|
doc = Doc(title="Calling the API")
|
|
doc.md("## Parameters")
|
|
doc.code("BASE_URL = 'https://example.invalid'")
|
|
doc.md("Run the cell above, then:")
|
|
doc.code("print(call('GET', '/health'))", live_only=True)
|
|
|
|
The reason the model is this thin: the same content has to come out as three
|
|
notebook variants that differ only in what executes, and as HTML later. Any
|
|
structure richer than "ordered blocks, each either prose or code" starts
|
|
encoding one format's assumptions into the shared layer, and then the variants
|
|
drift apart because each is really its own document.
|
|
|
|
`live_only` is the one concession. A block marked with it is dropped from the
|
|
vanilla and executable variants and kept in the live one — health checks,
|
|
timings, the things that are only meaningful against a running service.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class Block:
|
|
"""One cell's worth of content. `kind` is 'md' or 'code'."""
|
|
|
|
kind: str
|
|
text: str
|
|
live_only: bool = False
|
|
|
|
def lines(self) -> list[str]:
|
|
"""Source split the way notebook JSON wants it: newline kept, last line bare.
|
|
|
|
Not `text.splitlines()` — that drops the newlines, and a notebook whose
|
|
source lines have no `\\n` renders every cell as a single run-on line.
|
|
"""
|
|
parts = self.text.split("\n")
|
|
return [p + "\n" for p in parts[:-1]] + ([parts[-1]] if parts[-1] else [])
|
|
|
|
|
|
@dataclass
|
|
class Doc:
|
|
"""An ordered list of blocks, plus a title."""
|
|
|
|
title: str = ""
|
|
blocks: list[Block] = field(default_factory=list)
|
|
|
|
def md(self, text: str, live_only: bool = False) -> "Doc":
|
|
self.blocks.append(Block("md", text.strip("\n"), live_only))
|
|
return self
|
|
|
|
def code(self, text: str, live_only: bool = False) -> "Doc":
|
|
self.blocks.append(Block("code", text.strip("\n"), live_only))
|
|
return self
|
|
|
|
def for_variant(self, variant: str) -> list[Block]:
|
|
"""The blocks that belong in one variant.
|
|
|
|
vanilla / executable carry the same blocks — they differ in whether the
|
|
emitter marks the code as having run, not in what is written. Keeping
|
|
them one document is what stops the two from drifting into separate
|
|
hand-maintained files, which is how this usually goes wrong.
|
|
"""
|
|
if variant == "live":
|
|
return list(self.blocks)
|
|
return [b for b in self.blocks if not b.live_only]
|