metaframe-widget 0.1.0__py3-none-any.whl
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.
- metaframe_widget/__init__.py +3 -0
- metaframe_widget/_esm.py +111 -0
- metaframe_widget/_widget.py +83 -0
- metaframe_widget-0.1.0.dist-info/METADATA +107 -0
- metaframe_widget-0.1.0.dist-info/RECORD +7 -0
- metaframe_widget-0.1.0.dist-info/WHEEL +4 -0
- metaframe_widget-0.1.0.dist-info/licenses/LICENSE +21 -0
metaframe_widget/_esm.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
ESM = """
|
|
2
|
+
const CDN_URL = "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.6/+esm";
|
|
3
|
+
|
|
4
|
+
let _renderMetapage = null;
|
|
5
|
+
|
|
6
|
+
async function loadRenderMetapage() {
|
|
7
|
+
if (!_renderMetapage) {
|
|
8
|
+
const mod = await import(CDN_URL);
|
|
9
|
+
_renderMetapage = mod.renderMetapage;
|
|
10
|
+
}
|
|
11
|
+
return _renderMetapage;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async render({ model, el }) {
|
|
16
|
+
// Size el explicitly so marimo and Jupyter honour width/height
|
|
17
|
+
el.style.display = "block";
|
|
18
|
+
el.style.boxSizing = "border-box";
|
|
19
|
+
el.style.width = model.get("width") || "100%";
|
|
20
|
+
el.style.height = model.get("height") || "400px";
|
|
21
|
+
|
|
22
|
+
// Inject CSS to remove iframe borders and inline gaps
|
|
23
|
+
const style = document.createElement("style");
|
|
24
|
+
style.textContent = ".metaframe-widget-container { box-sizing: border-box; width: 100%; height: 100%; } .metaframe-widget-container iframe { border: none; display: block; margin: 0; padding: 0; box-sizing: border-box; width: 100%; height: 100%; }";
|
|
25
|
+
el.appendChild(style);
|
|
26
|
+
|
|
27
|
+
const container = document.createElement("div");
|
|
28
|
+
container.className = "metaframe-widget-container";
|
|
29
|
+
container.style.width = "100%";
|
|
30
|
+
container.style.height = "100%";
|
|
31
|
+
el.appendChild(container);
|
|
32
|
+
|
|
33
|
+
const renderMetapage = await loadRenderMetapage();
|
|
34
|
+
|
|
35
|
+
let currentResult = null;
|
|
36
|
+
|
|
37
|
+
function applyAllowToIframes() {
|
|
38
|
+
const allow = model.get("allow") || "";
|
|
39
|
+
if (allow) {
|
|
40
|
+
container.querySelectorAll("iframe").forEach(iframe => {
|
|
41
|
+
iframe.allow = allow;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function createMetapage() {
|
|
47
|
+
if (currentResult) {
|
|
48
|
+
currentResult.dispose();
|
|
49
|
+
currentResult = null;
|
|
50
|
+
}
|
|
51
|
+
container.innerHTML = "";
|
|
52
|
+
|
|
53
|
+
const url = model.get("url");
|
|
54
|
+
if (!url) return;
|
|
55
|
+
|
|
56
|
+
const definition = {
|
|
57
|
+
version: "0.3",
|
|
58
|
+
metaframes: {
|
|
59
|
+
mf: { url },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = await renderMetapage({
|
|
64
|
+
definition,
|
|
65
|
+
rootDiv: container,
|
|
66
|
+
onOutputs: (outputs) => {
|
|
67
|
+
const mfOutputs = outputs.mf || {};
|
|
68
|
+
model.set("outputs", JSON.parse(JSON.stringify(mfOutputs)));
|
|
69
|
+
model.save_changes();
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
currentResult = result;
|
|
73
|
+
|
|
74
|
+
applyAllowToIframes();
|
|
75
|
+
|
|
76
|
+
// Push current inputs if any
|
|
77
|
+
const inputs = model.get("inputs");
|
|
78
|
+
if (inputs && Object.keys(inputs).length > 0) {
|
|
79
|
+
result.setInputs({ mf: inputs });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
model.on("change:url", createMetapage);
|
|
84
|
+
|
|
85
|
+
model.on("change:inputs", () => {
|
|
86
|
+
if (currentResult) {
|
|
87
|
+
const inputs = model.get("inputs");
|
|
88
|
+
currentResult.setInputs({ mf: inputs });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
model.on("change:width", () => {
|
|
93
|
+
el.style.width = model.get("width") || "100%";
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
model.on("change:height", () => {
|
|
97
|
+
el.style.height = model.get("height") || "400px";
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
model.on("change:allow", applyAllowToIframes);
|
|
101
|
+
|
|
102
|
+
await createMetapage();
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
if (currentResult) {
|
|
106
|
+
currentResult.dispose();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
"""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import urllib.parse
|
|
3
|
+
|
|
4
|
+
import anywidget
|
|
5
|
+
import traitlets
|
|
6
|
+
|
|
7
|
+
from ._esm import ESM
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MetaframeWidget(anywidget.AnyWidget):
|
|
11
|
+
"""Widget that renders a metaframe iframe (works in Jupyter and marimo).
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
url: Full metaframe URL (including hash params).
|
|
15
|
+
inputs: Dict of inputs to push to the metaframe.
|
|
16
|
+
outputs: Dict of outputs received from the metaframe (read-only).
|
|
17
|
+
width: CSS width for the widget container (default "100%").
|
|
18
|
+
height: CSS height for the widget container (default "400px").
|
|
19
|
+
allow: iframe allow attribute string (e.g. "camera; microphone").
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_esm = ESM
|
|
23
|
+
|
|
24
|
+
url = traitlets.Unicode("").tag(sync=True)
|
|
25
|
+
inputs = traitlets.Dict({}).tag(sync=True)
|
|
26
|
+
outputs = traitlets.Dict({}).tag(sync=True)
|
|
27
|
+
width = traitlets.Unicode("100%").tag(sync=True)
|
|
28
|
+
height = traitlets.Unicode("400px").tag(sync=True)
|
|
29
|
+
allow = traitlets.Unicode("").tag(sync=True)
|
|
30
|
+
|
|
31
|
+
def set_inputs(self, d: dict):
|
|
32
|
+
"""Merge a dict into the current inputs."""
|
|
33
|
+
self.inputs = {**self.inputs, **d}
|
|
34
|
+
|
|
35
|
+
def set_input(self, key: str, value):
|
|
36
|
+
"""Set a single input key."""
|
|
37
|
+
self.inputs = {**self.inputs, key: value}
|
|
38
|
+
|
|
39
|
+
def on_outputs_change(self, callback):
|
|
40
|
+
"""Register a callback for when outputs change.
|
|
41
|
+
|
|
42
|
+
The callback receives a change dict with 'old' and 'new' keys.
|
|
43
|
+
"""
|
|
44
|
+
self.observe(callback, names=["outputs"])
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_code(
|
|
48
|
+
cls,
|
|
49
|
+
js_code: str,
|
|
50
|
+
width: str = "100%",
|
|
51
|
+
height: str = "400px",
|
|
52
|
+
allow: str = "",
|
|
53
|
+
**kwargs,
|
|
54
|
+
) -> "MetaframeWidget":
|
|
55
|
+
"""Create a MetaframeWidget from raw JavaScript code.
|
|
56
|
+
|
|
57
|
+
Encodes the code into a js.mtfm.io URL with the code in the hash.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
js_code: JavaScript source for the metaframe.
|
|
61
|
+
width: CSS width for the widget container (default "100%").
|
|
62
|
+
height: CSS height for the widget container (default "400px").
|
|
63
|
+
allow: iframe allow attribute string (e.g. "camera; microphone").
|
|
64
|
+
"""
|
|
65
|
+
encoded = base64.b64encode(js_code.encode()).decode()
|
|
66
|
+
url = f"https://js.mtfm.io/#?js={urllib.parse.quote(encoded)}"
|
|
67
|
+
return cls(url=url, width=width, height=height, allow=allow, **kwargs)
|
|
68
|
+
|
|
69
|
+
def pipe_to(self, target: "MetaframeWidget", output_key: str, input_key: str = None):
|
|
70
|
+
"""Connect an output of this widget to an input of another.
|
|
71
|
+
|
|
72
|
+
When this widget's output_key changes, push the value to
|
|
73
|
+
target's input_key (defaults to output_key if not specified).
|
|
74
|
+
"""
|
|
75
|
+
if input_key is None:
|
|
76
|
+
input_key = output_key
|
|
77
|
+
|
|
78
|
+
def _forward(change):
|
|
79
|
+
new_outputs = change.get("new", {})
|
|
80
|
+
if output_key in new_outputs:
|
|
81
|
+
target.set_input(input_key, new_outputs[output_key])
|
|
82
|
+
|
|
83
|
+
self.observe(_forward, names=["outputs"])
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metaframe-widget
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: anywidget for embedding metaframe URLs in Jupyter and marimo notebooks
|
|
5
|
+
Project-URL: Homepage, https://github.com/metapages/metaframe-js
|
|
6
|
+
Project-URL: Repository, https://github.com/metapages/metaframe-js
|
|
7
|
+
Project-URL: Issues, https://github.com/metapages/metaframe-js/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Framework :: Jupyter
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Requires-Dist: anywidget>=0.9.0
|
|
21
|
+
Requires-Dist: traitlets>=5.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
24
|
+
Provides-Extra: jupyter
|
|
25
|
+
Requires-Dist: jupyterlab; extra == 'jupyter'
|
|
26
|
+
Provides-Extra: marimo
|
|
27
|
+
Requires-Dist: marimo; extra == 'marimo'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# metaframe-widget
|
|
31
|
+
|
|
32
|
+
An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://metapages.org/) URLs in Jupyter and marimo notebooks.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install metaframe-widget
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
With environment extras:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install "metaframe-widget[jupyter]" # includes jupyterlab
|
|
44
|
+
pip install "metaframe-widget[marimo]" # includes marimo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
### Jupyter
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from metaframe_widget import MetaframeWidget
|
|
53
|
+
|
|
54
|
+
w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
|
|
55
|
+
w
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### marimo
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import marimo as mo
|
|
62
|
+
from metaframe_widget import MetaframeWidget
|
|
63
|
+
|
|
64
|
+
w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
|
|
65
|
+
mo.ui.anywidget(w)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Create from inline JavaScript
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
w = MetaframeWidget.from_code("""
|
|
72
|
+
setOutput("result", getInput("data") * 2);
|
|
73
|
+
""", height="300px")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API reference
|
|
77
|
+
|
|
78
|
+
| Parameter | Type | Default | Description |
|
|
79
|
+
|-----------|------|---------|-------------|
|
|
80
|
+
| `url` | `str` | `""` | Full metaframe URL (including hash params) |
|
|
81
|
+
| `inputs` | `dict` | `{}` | Dict of inputs to push to the metaframe |
|
|
82
|
+
| `outputs` | `dict` | `{}` | Dict of outputs received from the metaframe (read-only) |
|
|
83
|
+
| `width` | `str` | `"100%"` | CSS width for the widget container |
|
|
84
|
+
| `height` | `str` | `"400px"` | CSS height for the widget container |
|
|
85
|
+
| `allow` | `str` | `""` | iframe `allow` attribute (e.g. `"camera; microphone"`) |
|
|
86
|
+
|
|
87
|
+
### Methods
|
|
88
|
+
|
|
89
|
+
- **`set_inputs(d)`** — merge a dict into current inputs
|
|
90
|
+
- **`set_input(key, value)`** — set a single input key
|
|
91
|
+
- **`on_outputs_change(callback)`** — register a callback for output changes
|
|
92
|
+
- **`from_code(js_code, **kwargs)`** — class method to create a widget from inline JS
|
|
93
|
+
- **`pipe_to(target, output_key, input_key=None)`** — connect an output to another widget's input
|
|
94
|
+
|
|
95
|
+
## Piping widgets
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
source = MetaframeWidget(url="...")
|
|
99
|
+
sink = MetaframeWidget(url="...")
|
|
100
|
+
source.pipe_to(sink, output_key="result", input_key="data")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Links
|
|
104
|
+
|
|
105
|
+
- [GitHub](https://github.com/metapages/metaframe-js)
|
|
106
|
+
- [Jupyter examples](https://github.com/metapages/metaframe-js/tree/main/examples/jupyter)
|
|
107
|
+
- [marimo examples](https://github.com/metapages/metaframe-js/tree/main/examples/marimo)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
metaframe_widget/__init__.py,sha256=ktAsvMoLpUl9J0J8C0E8RShJrJRVN3M5_k6c_0xkLNc,68
|
|
2
|
+
metaframe_widget/_esm.py,sha256=wtcO35pLrTJQ9BFRC7QdRVn1d2eZSn2YL5RCuqL_61I,3468
|
|
3
|
+
metaframe_widget/_widget.py,sha256=sQmrUlYzWPpSnPqWfc0I9oiiB1vU6UyMPWT9f1P_VlM,2922
|
|
4
|
+
metaframe_widget-0.1.0.dist-info/METADATA,sha256=MdC4F65xxaYAOVQgg-4mEyrTdDVGdKHXN85uH9huT0w,3370
|
|
5
|
+
metaframe_widget-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
metaframe_widget-0.1.0.dist-info/licenses/LICENSE,sha256=3EllEi_PumemlXMuihY5tnXiVNjkJokA2wXOkjDufb8,1066
|
|
7
|
+
metaframe_widget-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Metapages
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|