pyobservablejs 0.0.1__tar.gz → 0.0.2__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 (56) hide show
  1. pyobservablejs-0.0.2/PKG-INFO +202 -0
  2. pyobservablejs-0.0.2/README.md +175 -0
  3. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/pyproject.toml +1 -1
  4. pyobservablejs-0.0.2/src/observablejs/_cell_ids.py +41 -0
  5. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_cells.py +57 -61
  6. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_graph.py +4 -2
  7. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_html.py +57 -11
  8. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_model.py +62 -47
  9. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_notebook.py +289 -428
  10. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_observable.py +59 -28
  11. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/anywidget.json +12 -12
  12. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/app.js +25556 -25560
  13. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-BFxwn1k_.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-B8rCnro7.js +1 -1
  14. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-CChK4Yoc.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-BBid0iVP.js +1 -1
  15. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-Bv2XdLMq.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-BcMmBf-x.js +1 -1
  16. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DxP0BJe8.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-BvgH-YoW.js +1 -1
  17. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DF8zms38.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-CNOag9jS.js +1 -1
  18. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-C-Zi_MOa.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-DNnAbIpY.js +2 -2
  19. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DoxPLY3X.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-DeGYBQTD.js +1 -1
  20. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-Bk-bxDe2.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-OUlIQxbR.js +2 -2
  21. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-jXhRULup.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-dCPcj_tB.js +1 -1
  22. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DyXgBxPb.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-l9UCRhqk.js +1 -1
  23. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-B6tLkwn1.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-ubwTYy8H.js +1 -1
  24. pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-C3vVwkXv.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-vMTWPMF2.js +4 -4
  25. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/index.js +2 -2
  26. pyobservablejs-0.0.1/PKG-INFO +0 -180
  27. pyobservablejs-0.0.1/README.md +0 -153
  28. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/LICENSE +0 -0
  29. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/NOTICE +0 -0
  30. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/hatch_build.py +0 -0
  31. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/__init__.py +0 -0
  32. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_files.py +0 -0
  33. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_graph_diagram.py +0 -0
  34. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_serialize.py +0 -0
  35. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_themes.py +0 -0
  36. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_variables.py +0 -0
  37. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/py.typed +0 -0
  38. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-30hes1yk.js +0 -0
  39. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-8yGiBzpe.js +0 -0
  40. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-9wrQFRY9.js +0 -0
  41. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BJBuA-nX.js +0 -0
  42. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BbYPAYwa.js +0 -0
  43. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-Bk0bJ4nQ.js +0 -0
  44. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BmowowmI.js +0 -0
  45. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-CE8y8yOz.js +0 -0
  46. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-CWz50zkY.js +0 -0
  47. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-ClxVhTBJ.js +0 -0
  48. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-D9CsW6Lj.js +0 -0
  49. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-DWy2KXvB.js +0 -0
  50. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-Da471Zoz.js +0 -0
  51. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-DvCCsF21.js +0 -0
  52. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-YNPpR7K-.js +0 -0
  53. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-eHnX08no.js +0 -0
  54. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-nUtTZkU8.js +0 -0
  55. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-t9a0frvQ.js +0 -0
  56. {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/widget.css +0 -0
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyobservablejs
3
+ Version: 0.0.2
4
+ Summary: Observable JavaScript notebooks as reusable Python widgets
5
+ Project-URL: Homepage, https://github.com/peter-gy/pyobservablejs
6
+ Project-URL: Documentation, https://peter-gy.github.io/pyobservablejs/
7
+ Project-URL: Source, https://github.com/peter-gy/pyobservablejs
8
+ Project-URL: Issues, https://github.com/peter-gy/pyobservablejs/issues
9
+ Author-email: Peter Ferenc Gyarmati <dev.petergy@gmail.com>
10
+ Maintainer-email: Peter Ferenc Gyarmati <dev.petergy@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: anywidget,jupyter,marimo,notebooks,observable,visualization
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: anywidget-bundle==0.0.1
24
+ Requires-Dist: anywidget>=0.11.0
25
+ Requires-Dist: traitlets>=5
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pyobservablejs
29
+
30
+ [![CI](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml/badge.svg)](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml)
31
+ [![PyPI](https://img.shields.io/pypi/v/pyobservablejs.svg)](https://pypi.org/project/pyobservablejs/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyobservablejs.svg)](https://pypi.org/project/pyobservablejs/)
33
+ [![License](https://img.shields.io/pypi/l/pyobservablejs.svg)](https://github.com/peter-gy/pyobservablejs/blob/main/LICENSE)
34
+
35
+ `pyobservablejs` brings interactive Observable notebooks into Python workflows.
36
+ Author a notebook in Python or load an existing one, render the views you need,
37
+ and keep its controls and results connected to your Python code.
38
+
39
+ Use it in JupyterLab, marimo, VS Code notebooks, Google Colab, and other
40
+ environments that support [anywidget](https://anywidget.dev/).
41
+
42
+ ## Quick start
43
+
44
+ Python 3.11 or newer is required.
45
+
46
+ ```sh
47
+ uv add pyobservablejs marimo
48
+ uv run marimo edit notebook.py
49
+ ```
50
+
51
+ ### Existing Observable notebook
52
+
53
+ ```python
54
+ import marimo as mo
55
+ import observablejs as obs
56
+
57
+ notebook = obs.Notebook.from_observablehq("@d3/world-tour")
58
+ mo.ui.anywidget(notebook.view())
59
+ ```
60
+
61
+ `@d3/world-tour` resolves to
62
+ [observablehq.com/@d3/world-tour](https://observablehq.com/@d3/world-tour).
63
+
64
+ ### Author an interactive notebook
65
+
66
+ ```python
67
+ import marimo as mo
68
+ import observablejs as obs
69
+
70
+ notebook = obs.Notebook(
71
+ obs.html(
72
+ """
73
+ <h2>Palmer penguins</h2>
74
+ <p>Choose a species to filter the chart.</p>
75
+ """
76
+ ),
77
+ obs.ojs(
78
+ """
79
+ viewof species = Inputs.select(
80
+ ["All", ...new Set(penguins.map((d) => d.species))],
81
+ {label: "Species", value: "All"}
82
+ )
83
+ """,
84
+ key="species_control",
85
+ ),
86
+ obs.js(
87
+ """
88
+ Plot.dot(
89
+ species === "All"
90
+ ? penguins
91
+ : penguins.filter((d) => d.species === species),
92
+ {
93
+ x: "culmen_length_mm",
94
+ y: "culmen_depth_mm",
95
+ fill: "species",
96
+ tip: true
97
+ }
98
+ ).plot({
99
+ height: 320,
100
+ color: {legend: true},
101
+ x: {grid: true, label: "Bill length (mm)"},
102
+ y: {grid: true, label: "Bill depth (mm)"}
103
+ })
104
+ """,
105
+ key="penguins_plot",
106
+ ),
107
+ )
108
+
109
+ mo.ui.anywidget(notebook.view())
110
+ ```
111
+
112
+ `penguins`, `Inputs`, and `Plot` come from Notebook Kit. Changing the `viewof`
113
+ selection recomputes the plot in the browser.
114
+
115
+ ## Cells, views, and inputs
116
+
117
+ `Notebook` accepts four kinds of cells:
118
+
119
+ | Helper | Cell source |
120
+ | --------------- | -------------------------------- |
121
+ | `obs.js(...)` | Standard Notebook Kit JavaScript |
122
+ | `obs.ojs(...)` | Observable JavaScript |
123
+ | `obs.md(...)` | Markdown |
124
+ | `obs.html(...)` | HTML |
125
+
126
+ Top-level declarations in JavaScript cells form a reactive graph: when a value
127
+ changes, every cell that references it runs again.
128
+
129
+ _View_ means two things. In Python, `.view()` creates a renderable
130
+ `NotebookView` widget. In a JavaScript cell, `view(input)` displays a browser
131
+ input and defines its reactive value.
132
+
133
+ | Call | Result |
134
+ | ----------------------------- | ----------------------------------------------------------------- |
135
+ | `notebook.view()` | A `NotebookView` that renders every cell. |
136
+ | `notebook.view(cells=[...])` | A composite view of selected cells and their hidden dependencies. |
137
+ | `notebook.cell(0).view()` | A focused view of one cell and its hidden dependencies. |
138
+ | `view(input)` inside `obs.js` | A browser input whose value other cells can reference. |
139
+
140
+ Create one view per place a notebook appears in the host. Views from the same
141
+ notebook stay connected: Python variable updates reach all of them, and named
142
+ `viewof` inputs share supported values across views.
143
+
144
+ ## Python variables
145
+
146
+ Pass Python values to the browser through `variables`:
147
+
148
+ ```python
149
+ import observablejs as obs
150
+
151
+ notebook = obs.Notebook(
152
+ obs.js('html`<p>Threshold: <strong>${threshold}</strong></p>`'),
153
+ variables={"threshold": 0.75},
154
+ )
155
+
156
+ full_view = notebook.view()
157
+ full_view
158
+ ```
159
+
160
+ While the view stays mounted, call `notebook.update_variables(threshold=0.9)`
161
+ to update the value.
162
+
163
+ After the view renders, read browser values through `values` or `cell_values`
164
+ and inspect dependency metadata through `graph`.
165
+
166
+ ## Load Notebook Kit HTML
167
+
168
+ Load a Notebook Kit HTML document:
169
+
170
+ ```python
171
+ from pathlib import Path
172
+
173
+ import observablejs as obs
174
+
175
+ path = Path("chart.html")
176
+ notebook = obs.Notebook.from_html(
177
+ path.read_text(encoding="utf-8"),
178
+ base_path=path.parent,
179
+ embed_file_attachments=True,
180
+ rewrite_imports=True,
181
+ )
182
+ ```
183
+
184
+ ## Documentation
185
+
186
+ - [Quickstart](https://peter-gy.github.io/pyobservablejs/quickstart/)
187
+ - [Mental model](https://peter-gy.github.io/pyobservablejs/mental-model/)
188
+ - [Recipes](https://peter-gy.github.io/pyobservablejs/recipes/)
189
+ - [API reference](https://peter-gy.github.io/pyobservablejs/reference/)
190
+
191
+ ## Acknowledgements
192
+
193
+ Thanks to the Observable team for [Notebook
194
+ Kit](https://github.com/observablehq/notebook-kit), which provides the notebook
195
+ APIs and runtime used throughout this project.
196
+ [`pyobsplot`](https://github.com/juba/pyobsplot) informed the Python variable
197
+ API, and [Trevor Manz](https://github.com/manzt)'s anywidget composition demo
198
+ shaped the widget design.
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,175 @@
1
+ # pyobservablejs
2
+
3
+ [![CI](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml/badge.svg)](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pyobservablejs.svg)](https://pypi.org/project/pyobservablejs/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyobservablejs.svg)](https://pypi.org/project/pyobservablejs/)
6
+ [![License](https://img.shields.io/pypi/l/pyobservablejs.svg)](https://github.com/peter-gy/pyobservablejs/blob/main/LICENSE)
7
+
8
+ `pyobservablejs` brings interactive Observable notebooks into Python workflows.
9
+ Author a notebook in Python or load an existing one, render the views you need,
10
+ and keep its controls and results connected to your Python code.
11
+
12
+ Use it in JupyterLab, marimo, VS Code notebooks, Google Colab, and other
13
+ environments that support [anywidget](https://anywidget.dev/).
14
+
15
+ ## Quick start
16
+
17
+ Python 3.11 or newer is required.
18
+
19
+ ```sh
20
+ uv add pyobservablejs marimo
21
+ uv run marimo edit notebook.py
22
+ ```
23
+
24
+ ### Existing Observable notebook
25
+
26
+ ```python
27
+ import marimo as mo
28
+ import observablejs as obs
29
+
30
+ notebook = obs.Notebook.from_observablehq("@d3/world-tour")
31
+ mo.ui.anywidget(notebook.view())
32
+ ```
33
+
34
+ `@d3/world-tour` resolves to
35
+ [observablehq.com/@d3/world-tour](https://observablehq.com/@d3/world-tour).
36
+
37
+ ### Author an interactive notebook
38
+
39
+ ```python
40
+ import marimo as mo
41
+ import observablejs as obs
42
+
43
+ notebook = obs.Notebook(
44
+ obs.html(
45
+ """
46
+ <h2>Palmer penguins</h2>
47
+ <p>Choose a species to filter the chart.</p>
48
+ """
49
+ ),
50
+ obs.ojs(
51
+ """
52
+ viewof species = Inputs.select(
53
+ ["All", ...new Set(penguins.map((d) => d.species))],
54
+ {label: "Species", value: "All"}
55
+ )
56
+ """,
57
+ key="species_control",
58
+ ),
59
+ obs.js(
60
+ """
61
+ Plot.dot(
62
+ species === "All"
63
+ ? penguins
64
+ : penguins.filter((d) => d.species === species),
65
+ {
66
+ x: "culmen_length_mm",
67
+ y: "culmen_depth_mm",
68
+ fill: "species",
69
+ tip: true
70
+ }
71
+ ).plot({
72
+ height: 320,
73
+ color: {legend: true},
74
+ x: {grid: true, label: "Bill length (mm)"},
75
+ y: {grid: true, label: "Bill depth (mm)"}
76
+ })
77
+ """,
78
+ key="penguins_plot",
79
+ ),
80
+ )
81
+
82
+ mo.ui.anywidget(notebook.view())
83
+ ```
84
+
85
+ `penguins`, `Inputs`, and `Plot` come from Notebook Kit. Changing the `viewof`
86
+ selection recomputes the plot in the browser.
87
+
88
+ ## Cells, views, and inputs
89
+
90
+ `Notebook` accepts four kinds of cells:
91
+
92
+ | Helper | Cell source |
93
+ | --------------- | -------------------------------- |
94
+ | `obs.js(...)` | Standard Notebook Kit JavaScript |
95
+ | `obs.ojs(...)` | Observable JavaScript |
96
+ | `obs.md(...)` | Markdown |
97
+ | `obs.html(...)` | HTML |
98
+
99
+ Top-level declarations in JavaScript cells form a reactive graph: when a value
100
+ changes, every cell that references it runs again.
101
+
102
+ _View_ means two things. In Python, `.view()` creates a renderable
103
+ `NotebookView` widget. In a JavaScript cell, `view(input)` displays a browser
104
+ input and defines its reactive value.
105
+
106
+ | Call | Result |
107
+ | ----------------------------- | ----------------------------------------------------------------- |
108
+ | `notebook.view()` | A `NotebookView` that renders every cell. |
109
+ | `notebook.view(cells=[...])` | A composite view of selected cells and their hidden dependencies. |
110
+ | `notebook.cell(0).view()` | A focused view of one cell and its hidden dependencies. |
111
+ | `view(input)` inside `obs.js` | A browser input whose value other cells can reference. |
112
+
113
+ Create one view per place a notebook appears in the host. Views from the same
114
+ notebook stay connected: Python variable updates reach all of them, and named
115
+ `viewof` inputs share supported values across views.
116
+
117
+ ## Python variables
118
+
119
+ Pass Python values to the browser through `variables`:
120
+
121
+ ```python
122
+ import observablejs as obs
123
+
124
+ notebook = obs.Notebook(
125
+ obs.js('html`<p>Threshold: <strong>${threshold}</strong></p>`'),
126
+ variables={"threshold": 0.75},
127
+ )
128
+
129
+ full_view = notebook.view()
130
+ full_view
131
+ ```
132
+
133
+ While the view stays mounted, call `notebook.update_variables(threshold=0.9)`
134
+ to update the value.
135
+
136
+ After the view renders, read browser values through `values` or `cell_values`
137
+ and inspect dependency metadata through `graph`.
138
+
139
+ ## Load Notebook Kit HTML
140
+
141
+ Load a Notebook Kit HTML document:
142
+
143
+ ```python
144
+ from pathlib import Path
145
+
146
+ import observablejs as obs
147
+
148
+ path = Path("chart.html")
149
+ notebook = obs.Notebook.from_html(
150
+ path.read_text(encoding="utf-8"),
151
+ base_path=path.parent,
152
+ embed_file_attachments=True,
153
+ rewrite_imports=True,
154
+ )
155
+ ```
156
+
157
+ ## Documentation
158
+
159
+ - [Quickstart](https://peter-gy.github.io/pyobservablejs/quickstart/)
160
+ - [Mental model](https://peter-gy.github.io/pyobservablejs/mental-model/)
161
+ - [Recipes](https://peter-gy.github.io/pyobservablejs/recipes/)
162
+ - [API reference](https://peter-gy.github.io/pyobservablejs/reference/)
163
+
164
+ ## Acknowledgements
165
+
166
+ Thanks to the Observable team for [Notebook
167
+ Kit](https://github.com/observablehq/notebook-kit), which provides the notebook
168
+ APIs and runtime used throughout this project.
169
+ [`pyobsplot`](https://github.com/juba/pyobsplot) informed the Python variable
170
+ API, and [Trevor Manz](https://github.com/manzt)'s anywidget composition demo
171
+ shaped the widget design.
172
+
173
+ ## License
174
+
175
+ MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyobservablejs"
7
- version = "0.0.1"
7
+ version = "0.0.2"
8
8
  description = "Observable JavaScript notebooks as reusable Python widgets"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,41 @@
1
+ """JavaScript-safe notebook cell ID allocation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+
7
+ _MAX_SAFE_CELL_ID = (1 << 53) - 1
8
+
9
+
10
+ def _is_safe_cell_id(value: object) -> bool:
11
+ return (
12
+ isinstance(value, int)
13
+ and not isinstance(value, bool)
14
+ and 1 <= value <= _MAX_SAFE_CELL_ID
15
+ )
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class _CellIdAllocator:
20
+ reserved: set[int]
21
+ next_id: int = 1
22
+
23
+ def advance_past(self, cell_id: int) -> None:
24
+ if cell_id >= self.next_id:
25
+ self.next_id = cell_id + 1
26
+
27
+ def allocate(self) -> int:
28
+ cell_id = self._find(self.next_id, _MAX_SAFE_CELL_ID)
29
+ if cell_id is None:
30
+ cell_id = self._find(1, min(self.next_id - 1, _MAX_SAFE_CELL_ID))
31
+ if cell_id is None:
32
+ raise ValueError("Notebook has no available JavaScript-safe cell id")
33
+ self.reserved.add(cell_id)
34
+ self.next_id = cell_id + 1
35
+ return cell_id
36
+
37
+ def _find(self, start: int, stop: int) -> int | None:
38
+ candidate = max(start, 1)
39
+ while candidate <= stop and candidate in self.reserved:
40
+ candidate += 1
41
+ return candidate if candidate <= stop else None
@@ -7,6 +7,7 @@ import textwrap
7
7
  from collections.abc import Mapping
8
8
  from typing import Any, NotRequired, TypedDict, cast
9
9
 
10
+ from ._cell_ids import _MAX_SAFE_CELL_ID, _is_safe_cell_id
10
11
  from ._serialize import SCRIPT_TYPES, Mode
11
12
 
12
13
 
@@ -27,8 +28,7 @@ class Cell:
27
28
  """Notebook Kit cell authored from Python source.
28
29
 
29
30
  Source strings are dedented and stripped of leading or trailing newlines
30
- unless ``raw=True``. ``to_spec`` returns the JSON shape consumed by Notebook
31
- Kit and by the bundled anywidget renderer.
31
+ unless ``raw=True``.
32
32
  """
33
33
 
34
34
  source: str
@@ -37,24 +37,47 @@ class Cell:
37
37
  name: str | None = None
38
38
  display: bool = True
39
39
  raw: bool = False
40
+ id: int | None = None
41
+ pinned: bool = False
42
+ output: str | None = None
40
43
  notebookkit_attrs: Mapping[str, Any] = dataclasses.field(default_factory=dict)
41
44
 
42
45
  def __post_init__(self) -> None:
43
46
  if not isinstance(self.source, str):
44
47
  raise TypeError("cell source must be a string")
48
+ if self.mode not in SCRIPT_TYPES:
49
+ raise ValueError(f"Unsupported Observable cell mode: {self.mode!r}")
50
+ if self.id is not None and (
51
+ not isinstance(self.id, int) or isinstance(self.id, bool)
52
+ ):
53
+ raise TypeError("cell id must be an integer or None")
54
+ if self.id is not None and not _is_safe_cell_id(self.id):
55
+ raise ValueError(f"cell id must be between 1 and {_MAX_SAFE_CELL_ID}")
56
+ attrs = dict(self.notebookkit_attrs)
57
+ collisions = {
58
+ "hidden",
59
+ "id",
60
+ "key",
61
+ "mode",
62
+ "name",
63
+ "output",
64
+ "pinned",
65
+ "value",
66
+ } & set(attrs)
67
+ if collisions:
68
+ names = ", ".join(sorted(collisions))
69
+ raise ValueError(
70
+ f"notebookkit_attrs cannot override first-class cell options: {names}"
71
+ )
72
+ object.__setattr__(self, "notebookkit_attrs", attrs)
45
73
  if self.raw:
46
74
  return
47
75
  source = textwrap.dedent(self.source).strip("\n")
48
76
  object.__setattr__(self, "source", source)
49
77
 
50
- def to_spec(self, id: int) -> NotebookCellSpec:
51
- """Return this cell in Notebook Kit's JSON cell shape."""
52
-
53
- if self.mode not in SCRIPT_TYPES:
54
- raise ValueError(f"Unsupported Observable cell mode: {self.mode!r}")
55
- attrs = dict(self.notebookkit_attrs)
78
+ def _to_spec(self, generated_id: int) -> NotebookCellSpec:
56
79
  spec: dict[str, Any] = {
57
- "id": attrs.pop("id", id),
80
+ "id": self.id if self.id is not None else generated_id,
58
81
  "value": self.source,
59
82
  "mode": self.mode,
60
83
  }
@@ -62,20 +85,24 @@ class Cell:
62
85
  spec["name"] = self.name
63
86
  if not self.display:
64
87
  spec["hidden"] = True
65
- for key, value in attrs.items():
88
+ if self.pinned:
89
+ spec["pinned"] = True
90
+ if self.output is not None:
91
+ spec["output"] = self.output
92
+ for key, value in self.notebookkit_attrs.items():
66
93
  if value is not None:
67
94
  spec[key] = value
68
95
  return cast(NotebookCellSpec, spec)
69
96
 
70
97
 
71
- CellInput = str | Cell
72
98
  NotebookCellInput = Cell
73
99
 
74
100
 
75
101
  def ojs(
76
- source: CellInput,
102
+ source: str,
77
103
  *,
78
104
  key: str | None = None,
105
+ name: str | None = None,
79
106
  display: bool = True,
80
107
  raw: bool = False,
81
108
  id: int | None = None,
@@ -89,6 +116,7 @@ def ojs(
89
116
  source,
90
117
  mode="ojs",
91
118
  key=key,
119
+ name=name,
92
120
  display=display,
93
121
  raw=raw,
94
122
  id=id,
@@ -99,9 +127,10 @@ def ojs(
99
127
 
100
128
 
101
129
  def js(
102
- source: CellInput,
130
+ source: str,
103
131
  *,
104
132
  key: str | None = None,
133
+ name: str | None = None,
105
134
  display: bool = True,
106
135
  raw: bool = False,
107
136
  id: int | None = None,
@@ -115,6 +144,7 @@ def js(
115
144
  source,
116
145
  mode="js",
117
146
  key=key,
147
+ name=name,
118
148
  display=display,
119
149
  raw=raw,
120
150
  id=id,
@@ -125,9 +155,10 @@ def js(
125
155
 
126
156
 
127
157
  def md(
128
- source: CellInput,
158
+ source: str,
129
159
  *,
130
160
  key: str | None = None,
161
+ name: str | None = None,
131
162
  display: bool = True,
132
163
  raw: bool = False,
133
164
  id: int | None = None,
@@ -141,6 +172,7 @@ def md(
141
172
  source,
142
173
  mode="md",
143
174
  key=key,
175
+ name=name,
144
176
  display=display,
145
177
  raw=raw,
146
178
  id=id,
@@ -151,9 +183,10 @@ def md(
151
183
 
152
184
 
153
185
  def html(
154
- source: CellInput,
186
+ source: str,
155
187
  *,
156
188
  key: str | None = None,
189
+ name: str | None = None,
157
190
  display: bool = True,
158
191
  raw: bool = False,
159
192
  id: int | None = None,
@@ -167,6 +200,7 @@ def html(
167
200
  source,
168
201
  mode="html",
169
202
  key=key,
203
+ name=name,
170
204
  display=display,
171
205
  raw=raw,
172
206
  id=id,
@@ -177,10 +211,11 @@ def html(
177
211
 
178
212
 
179
213
  def _source_cell(
180
- source: CellInput,
214
+ source: str,
181
215
  *,
182
216
  mode: Mode,
183
217
  key: str | None = None,
218
+ name: str | None = None,
184
219
  display: bool = True,
185
220
  raw: bool = False,
186
221
  id: int | None = None,
@@ -188,35 +223,19 @@ def _source_cell(
188
223
  output: str | None = None,
189
224
  notebookkit_attrs: Mapping[str, Any] | None = None,
190
225
  ) -> Cell:
191
- if isinstance(source, Cell):
192
- if any(
193
- [
194
- key is not None,
195
- display is not True,
196
- mode != source.mode,
197
- raw,
198
- id is not None,
199
- pinned,
200
- output is not None,
201
- notebookkit_attrs,
202
- ]
203
- ):
204
- raise TypeError("Cannot override an existing Cell")
205
- return source
206
226
  if not isinstance(source, str):
207
- raise TypeError("notebook cells must be strings or Cell objects")
227
+ raise TypeError("cell source must be a string")
208
228
  return Cell(
209
229
  source=source,
210
230
  mode=mode,
211
231
  key=key,
232
+ name=name,
212
233
  display=display,
213
234
  raw=raw,
214
- notebookkit_attrs=_cell_attrs(
215
- notebookkit_attrs,
216
- id=id,
217
- pinned=pinned,
218
- output=output,
219
- ),
235
+ id=id,
236
+ pinned=pinned,
237
+ output=output,
238
+ notebookkit_attrs={} if notebookkit_attrs is None else notebookkit_attrs,
220
239
  )
221
240
 
222
241
 
@@ -226,26 +245,3 @@ def coerce_cell(source: NotebookCellInput) -> Cell:
226
245
  raise TypeError(
227
246
  "notebook cells must be created with obs.ojs, obs.js, obs.md, or obs.html"
228
247
  )
229
-
230
-
231
- def _cell_attrs(
232
- notebookkit_attrs: Mapping[str, Any] | None,
233
- *,
234
- id: int | None,
235
- pinned: bool,
236
- output: str | None,
237
- ) -> dict[str, Any]:
238
- out = {} if notebookkit_attrs is None else dict(notebookkit_attrs)
239
- collisions = {"hidden", "id", "mode", "output", "pinned", "value"} & set(out)
240
- if collisions:
241
- names = ", ".join(sorted(collisions))
242
- raise ValueError(
243
- f"notebookkit_attrs cannot override first-class cell options: {names}"
244
- )
245
- if id is not None:
246
- out["id"] = id
247
- if pinned:
248
- out["pinned"] = True
249
- if output is not None:
250
- out["output"] = output
251
- return out
@@ -66,11 +66,13 @@ class NotebookGraph:
66
66
  defined.update(name for cell in self.cells for name in cell.runtime_outputs)
67
67
  return tuple(name for name in self.references if name not in defined)
68
68
 
69
- def cell(self, index: int) -> CellInfo | None:
69
+ def cell(self, index: int) -> CellInfo:
70
+ """Return the cell at ``index`` in the notebook definition."""
71
+
70
72
  for cell in self.cells:
71
73
  if cell.index == index:
72
74
  return cell
73
- return None
75
+ raise IndexError("notebook graph cell index out of range")
74
76
 
75
77
  def cell_for_variable(self, variable: str) -> CellInfo:
76
78
  matches = [cell for cell in self.cells if variable in cell.defines]