omle-viewer 0.5.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.
@@ -0,0 +1,10 @@
1
+ from .display import OMLEDisplay, model_repr_html, show, show_in_browser
2
+
3
+ try:
4
+ from ._version import (
5
+ __version__, # generated by setuptools-scm at build time
6
+ )
7
+ except ImportError: # pragma: no cover - source checkout without a build
8
+ __version__ = "0.0.0+unknown"
9
+
10
+ __all__ = ["OMLEDisplay", "show", "show_in_browser", "model_repr_html", "__version__"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.5.0'
22
+ __version_tuple__ = version_tuple = (0, 5, 0)
23
+
24
+ __commit_id__ = commit_id = None
omle_viewer/cli.py ADDED
@@ -0,0 +1,67 @@
1
+ """CLI entry point: ``omle-viewer [model-file]``
2
+
3
+ Opens an OMLE model file in the default web browser using the interactive
4
+ DAG viewer. Supports JSON (.json) and protobuf binary (.omle, .pb, .bin)
5
+ file formats.
6
+
7
+ The model file is optional: with no argument the viewer opens on its drop
8
+ target, where a file can be dragged in or JSON pasted.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser(
20
+ prog="omle-viewer",
21
+ description="Open an OMLE model in the interactive DAG viewer.",
22
+ )
23
+ parser.add_argument(
24
+ "model",
25
+ metavar="MODEL_FILE",
26
+ nargs="?",
27
+ help="Path to the model file (.json, .omle, .pb, .bin). "
28
+ "Omit to open the viewer empty and drop a file in.",
29
+ )
30
+ args = parser.parse_args()
31
+
32
+ from .display import show_in_browser
33
+
34
+ # No file: open the viewer on its drop target. Nothing here needs the
35
+ # `omle` package either, since there is no model to decode.
36
+ if args.model is None:
37
+ show_in_browser()
38
+ return
39
+
40
+ path = Path(args.model)
41
+ if not path.exists():
42
+ print(f"omle-viewer: file not found: {path}", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ try:
46
+ import omle
47
+ except ImportError:
48
+ print(
49
+ "omle-viewer: the 'omle' package is required. "
50
+ "Install it with: pip install omle",
51
+ file=sys.stderr,
52
+ )
53
+ sys.exit(1)
54
+
55
+ # omle.load dispatches on the extension and decodes both encodings; omle
56
+ # depends on protobuf directly, so binary files need no optional extra.
57
+ try:
58
+ model = omle.load(path)
59
+ except ValueError as e:
60
+ print(f"omle-viewer: {e}", file=sys.stderr)
61
+ sys.exit(1)
62
+
63
+ show_in_browser(model)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
omle_viewer/display.py ADDED
@@ -0,0 +1,182 @@
1
+ """Jupyter display helpers for OMLE models.
2
+
3
+ Primary approach: self-contained HTML iframe (works in every Jupyter environment
4
+ without any widget extensions — JupyterLab, Classic Notebook, VS Code, Colab).
5
+
6
+ The viewer HTML is built by running ``./build.sh`` at the repo root and embedded
7
+ as a base64 data-URI iframe so it needs no external server or asset loading.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from omle.ir.model import OMLEModel
18
+
19
+ _STATIC = Path(__file__).parent / "static"
20
+ _VIEWER_HTML = _STATIC / "viewer.html"
21
+
22
+ # ── HTML helpers ──────────────────────────────────────────────────────────────
23
+
24
+ _NOT_BUILT_HTML = """
25
+ <div style="padding:14px 16px;font-family:monospace;font-size:12px;
26
+ color:#b00;border:1px solid #f5c0c0;border-radius:6px;background:#fff5f5">
27
+ <strong>omle-viewer:</strong> The viewer bundle has not been built yet.<br><br>
28
+ Run the following from the repo root, then reinstall the package:<br>
29
+ <code style="display:block;margin-top:6px;padding:6px 8px;background:#fff;
30
+ border:1px solid #ddd;border-radius:4px">
31
+ ./build.sh<br>
32
+ pip install -e omle-viewer/python/
33
+ </code>
34
+ </div>
35
+ """
36
+
37
+
38
+ def _build_viewer_html(model: OMLEModel | None) -> str | None:
39
+ """Return the full self-contained viewer HTML, or None if the bundle is not built.
40
+
41
+ With *model* given it is injected as ``window.__OMLE_MODEL__`` and the
42
+ viewer opens on it. With *model* None nothing is injected and the viewer
43
+ opens on its drop target, ready for a file to be dragged in or JSON pasted
44
+ — the app already handles the model being absent, so there is no reason to
45
+ require one just to get a window open.
46
+ """
47
+ if not _VIEWER_HTML.exists():
48
+ return None
49
+ viewer = _VIEWER_HTML.read_text(encoding="utf-8")
50
+ if model is None:
51
+ return viewer
52
+ model_json = json.dumps(model.to_dict())
53
+ inject = f"<script>window.__OMLE_MODEL__ = {model_json};</script>"
54
+ return viewer.replace("</head>", inject + "</head>", 1)
55
+
56
+
57
+ def _build_iframe_html(model: OMLEModel, height: int) -> str:
58
+ """Return an ``<iframe srcdoc=...>`` HTML string that embeds the full interactive viewer.
59
+
60
+ Uses srcdoc instead of a data: URI because JupyterLab's CSP blocks data: iframes.
61
+ """
62
+ viewer = _build_viewer_html(model)
63
+ if viewer is None:
64
+ return _NOT_BUILT_HTML
65
+ import html as _html
66
+ # srcdoc is same-origin with the parent page so CSP `unsafe-inline` covers it;
67
+ # data: URIs are blocked by JupyterLab's frame-src CSP directive.
68
+ encoded = _html.escape(viewer, quote=True)
69
+ return (
70
+ f'<iframe srcdoc="{encoded}" '
71
+ f'style="width:100%;height:{height}px;border:none;border-radius:4px;" '
72
+ f'allowfullscreen>'
73
+ f"</iframe>"
74
+ )
75
+
76
+
77
+ def show_in_browser(model: OMLEModel | None = None) -> None:
78
+ """Open the interactive DAG viewer in the default web browser.
79
+
80
+ Writes a temporary self-contained HTML file and opens it via
81
+ ``webbrowser.open()``. Works from a script, a terminal, or a notebook.
82
+
83
+ Parameters
84
+ ----------
85
+ model:
86
+ The OMLEModel instance to visualize, or None to open the viewer empty
87
+ on its drop target.
88
+ """
89
+ import tempfile
90
+ import webbrowser
91
+
92
+ viewer = _build_viewer_html(model)
93
+ if viewer is None:
94
+ raise RuntimeError(
95
+ "omle-viewer: viewer bundle not built. "
96
+ "Run ./build.sh at the repo root first."
97
+ )
98
+
99
+ with tempfile.NamedTemporaryFile(
100
+ suffix=".html", delete=False, mode="w", encoding="utf-8"
101
+ ) as f:
102
+ f.write(viewer)
103
+ tmp_path = f.name
104
+
105
+ webbrowser.open(f"file://{tmp_path}")
106
+
107
+
108
+ # ── Public API ────────────────────────────────────────────────────────────────
109
+
110
+ class OMLEDisplay:
111
+ """Jupyter display object wrapping an OMLEModel.
112
+
113
+ Returned by ``show()`` so callers have a reference, but suppresses the
114
+ cell's auto-display of the return value (the iframe was already shown by
115
+ the explicit ``IPython.display.display()`` call inside ``show()``).
116
+ """
117
+
118
+ def __init__(self, model: OMLEModel, height: int = 500) -> None:
119
+ self._model = model
120
+ self._height = height
121
+
122
+ # Suppress Jupyter's implicit display of the return value.
123
+ def _repr_mimebundle_(self, **_kwargs):
124
+ return {}
125
+
126
+ def __repr__(self) -> str: # plain-Python fallback
127
+ return "<OMLEDisplay — call .show() to redisplay>"
128
+
129
+ def show(self, height: int | None = None) -> None:
130
+ """Re-display the viewer (e.g. after calling .update())."""
131
+ try:
132
+ import warnings
133
+
134
+ from IPython.display import HTML, display
135
+ with warnings.catch_warnings():
136
+ warnings.simplefilter("ignore", UserWarning)
137
+ display(HTML(_build_iframe_html(self._model, height or self._height)))
138
+ except ImportError:
139
+ pass
140
+
141
+ def update(self, model: OMLEModel, *, height: int | None = None) -> None:
142
+ """Display an updated model in a new cell output."""
143
+ self._model = model
144
+ if height is not None:
145
+ self._height = height
146
+ self.show()
147
+
148
+
149
+ def model_repr_html(model: OMLEModel, height: int = 500) -> str:
150
+ """Return the ``_repr_html_`` string for *model* (used by the display hook)."""
151
+ return _build_iframe_html(model, height)
152
+
153
+
154
+ def show(model: OMLEModel, height: int = 500) -> OMLEDisplay:
155
+ """Display an OMLEModel DAG inline in a Jupyter notebook.
156
+
157
+ Importing ``omle_viewer`` already makes models auto-render when
158
+ returned from a cell, so ``show()`` is mainly useful when you want a
159
+ non-default height or a handle for later updates::
160
+
161
+ w = show(model, height=700)
162
+ # push a new model without creating a new cell output:
163
+ w.update(updated_model)
164
+
165
+ Parameters
166
+ ----------
167
+ model:
168
+ The OMLEModel instance to visualize.
169
+ height:
170
+ Viewer height in pixels (default 500).
171
+ """
172
+ handle = OMLEDisplay(model, height)
173
+ try:
174
+ import warnings
175
+
176
+ from IPython.display import HTML, display
177
+ with warnings.catch_warnings():
178
+ warnings.simplefilter("ignore", UserWarning)
179
+ display(HTML(_build_iframe_html(model, height)))
180
+ except ImportError:
181
+ pass
182
+ return handle