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.
- pyobservablejs-0.0.2/PKG-INFO +202 -0
- pyobservablejs-0.0.2/README.md +175 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/pyproject.toml +1 -1
- pyobservablejs-0.0.2/src/observablejs/_cell_ids.py +41 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_cells.py +57 -61
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_graph.py +4 -2
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_html.py +57 -11
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_model.py +62 -47
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_notebook.py +289 -428
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_observable.py +59 -28
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/anywidget.json +12 -12
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/app.js +25556 -25560
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-BFxwn1k_.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-B8rCnro7.js +1 -1
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-CChK4Yoc.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-BBid0iVP.js +1 -1
- 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
- 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
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DF8zms38.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-CNOag9jS.js +1 -1
- 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
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DoxPLY3X.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-DeGYBQTD.js +1 -1
- 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
- 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
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-DyXgBxPb.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-l9UCRhqk.js +1 -1
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-B6tLkwn1.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-ubwTYy8H.js +1 -1
- pyobservablejs-0.0.1/src/observablejs/static/chunks/chunk-C3vVwkXv.js → pyobservablejs-0.0.2/src/observablejs/static/chunks/chunk-vMTWPMF2.js +4 -4
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/index.js +2 -2
- pyobservablejs-0.0.1/PKG-INFO +0 -180
- pyobservablejs-0.0.1/README.md +0 -153
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/LICENSE +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/NOTICE +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/hatch_build.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/__init__.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_files.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_graph_diagram.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_serialize.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_themes.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/_variables.py +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/py.typed +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-30hes1yk.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-8yGiBzpe.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-9wrQFRY9.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BJBuA-nX.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BbYPAYwa.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-Bk0bJ4nQ.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-BmowowmI.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-CE8y8yOz.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-CWz50zkY.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-ClxVhTBJ.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-D9CsW6Lj.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-DWy2KXvB.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-Da471Zoz.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-DvCCsF21.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-YNPpR7K-.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-eHnX08no.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-nUtTZkU8.js +0 -0
- {pyobservablejs-0.0.1 → pyobservablejs-0.0.2}/src/observablejs/static/chunks/chunk-t9a0frvQ.js +0 -0
- {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
|
+
[](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml)
|
|
31
|
+
[](https://pypi.org/project/pyobservablejs/)
|
|
32
|
+
[](https://pypi.org/project/pyobservablejs/)
|
|
33
|
+
[](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
|
+
[](https://github.com/peter-gy/pyobservablejs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pyobservablejs/)
|
|
5
|
+
[](https://pypi.org/project/pyobservablejs/)
|
|
6
|
+
[](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
|
|
@@ -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``.
|
|
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
|
|
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":
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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("
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
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
|
-
|
|
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]
|