seqeyes-python 0.2.1__py3-none-any.whl → 0.2.6__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.
seqeyes/__init__.py CHANGED
@@ -29,4 +29,4 @@ from seqeyes._plot import set, reset
29
29
  from seqeyes._renderer import SeqEyesViewer
30
30
 
31
31
  __all__ = ["set", "reset", "SeqEyesViewer"]
32
- __version__ = "0.2.1"
32
+ __version__ = "0.2.6"
seqeyes/_plot.py CHANGED
@@ -20,6 +20,9 @@ from typing import Any, Optional, Tuple, Union
20
20
 
21
21
  _defaults: dict[str, Any] = {}
22
22
  _original_plot = None
23
+ _original_repr_html = None
24
+ _had_original_repr_html = False
25
+ _patched_repr_html = None
23
26
  _patched = False
24
27
 
25
28
 
@@ -53,7 +56,7 @@ def reset() -> None:
53
56
 
54
57
  Call ``seqeyes.set()`` again to re‑enable at any time.
55
58
  """
56
- global _patched
59
+ global _patched, _patched_repr_html
57
60
  _defaults.clear()
58
61
  _patched = False
59
62
 
@@ -64,6 +67,12 @@ def reset() -> None:
64
67
 
65
68
  if _original_plot is not None:
66
69
  _Seq.plot = _original_plot # type: ignore[attr-defined]
70
+ if _patched_repr_html is not None and getattr(_Seq, "_repr_html_", None) is _patched_repr_html:
71
+ if _had_original_repr_html:
72
+ _Seq._repr_html_ = _original_repr_html # type: ignore[attr-defined]
73
+ else:
74
+ delattr(_Seq, "_repr_html_")
75
+ _patched_repr_html = None
67
76
 
68
77
 
69
78
  # ── Internal helpers ──────────────────────────────────────────────────────
@@ -87,7 +96,7 @@ def _store_defaults(
87
96
 
88
97
 
89
98
  def _ensure_patched() -> None:
90
- global _patched, _original_plot
99
+ global _patched, _original_plot, _original_repr_html, _had_original_repr_html, _patched_repr_html
91
100
 
92
101
  if _patched:
93
102
  return
@@ -100,6 +109,9 @@ def _ensure_patched() -> None:
100
109
 
101
110
  if _original_plot is None:
102
111
  _original_plot = getattr(_Seq, "plot", None)
112
+ if _patched_repr_html is None:
113
+ _had_original_repr_html = hasattr(_Seq, "_repr_html_")
114
+ _original_repr_html = getattr(_Seq, "_repr_html_", None)
103
115
 
104
116
  # ── Replacement .plot() ──────────────────────────────────────────
105
117
  def _seqeyes_plot(
@@ -108,7 +120,7 @@ def _ensure_patched() -> None:
108
120
  show_blocks: bool = False,
109
121
  time_range: Any = (0, float("inf")),
110
122
  time_disp: str = "s",
111
- grad_disp: str = "kHz/m",
123
+ grad_disp: str = "Hz/m",
112
124
  **kwargs: Any,
113
125
  ) -> None:
114
126
  # Merge globals — per‑call args take precedence
@@ -147,6 +159,7 @@ def _ensure_patched() -> None:
147
159
  return SeqEyesViewer(_seq_to_text(self))._repr_html_()
148
160
 
149
161
  _Seq._repr_html_ = _seq_repr_html_ # type: ignore[attr-defined]
162
+ _patched_repr_html = _seq_repr_html_
150
163
 
151
164
 
152
165
  def _in_jupyter() -> bool:
seqeyes/_renderer.py CHANGED
@@ -12,7 +12,7 @@ import base64
12
12
  import json
13
13
  import os
14
14
  from pathlib import Path
15
- from typing import Optional
15
+ from typing import Any, Optional, Union
16
16
 
17
17
 
18
18
  # ── Paths ─────────────────────────────────────────────────────────────────
@@ -26,6 +26,8 @@ _BUNDLE_CANDIDATES = [
26
26
  Path(__file__).resolve().parent.parent.parent.parent / "web" / "pulseq-bundle.js",
27
27
  Path.cwd() / "web" / "pulseq-bundle.js",
28
28
  ]
29
+ _VALID_GRAD_UNITS = {"Hz/m", "mT/m", "G/cm"}
30
+ SequenceSource = Union[str, bytes, bytearray, memoryview]
29
31
 
30
32
 
31
33
  def _find_bundle() -> str:
@@ -53,8 +55,13 @@ def _read_viewer_template() -> str:
53
55
  )
54
56
 
55
57
 
58
+ def _normalize_grad_disp(unit: str) -> str:
59
+ """Return a viewer-supported gradient unit."""
60
+ return unit if unit in _VALID_GRAD_UNITS else "Hz/m"
61
+
62
+
56
63
  def _build_html(
57
- seq_text: str,
64
+ sequence_source: SequenceSource,
58
65
  *,
59
66
  theme: Optional[str] = None,
60
67
  inject_bundle: bool = True,
@@ -62,14 +69,14 @@ def _build_html(
62
69
  show_blocks: bool = False,
63
70
  time_range: tuple = (0, float("inf")),
64
71
  time_disp: str = "s",
65
- grad_disp: str = "kHz/m",
72
+ grad_disp: str = "Hz/m",
66
73
  ) -> str:
67
74
  """Assemble the complete viewer HTML.
68
75
 
69
76
  Parameters
70
77
  ----------
71
- seq_text : str
72
- Raw .seq file content.
78
+ sequence_source : str or bytes-like
79
+ Raw text ``.seq`` content or binary ``.bseq`` bytes.
73
80
  theme : str or None
74
81
  CSS theme class to apply to ``<body>``.
75
82
  inject_bundle : bool
@@ -83,11 +90,13 @@ def _build_html(
83
90
  time_disp : str
84
91
  Initial time display unit (``"s"``, ``"ms"``, ``"us"``).
85
92
  grad_disp : str
86
- Initial gradient display unit (``"Hz/m"``, ``"kHz/m"``,
87
- ``"mT/m"``, ``"G/cm"``).
93
+ Initial gradient display unit (``"Hz/m"``, ``"mT/m"``, ``"G/cm"``).
88
94
  """
89
95
  template = _read_viewer_template()
90
- seq_b64 = base64.b64encode(seq_text.encode("utf-8")).decode("ascii")
96
+ grad_disp = _normalize_grad_disp(grad_disp)
97
+ source_is_text = isinstance(sequence_source, str)
98
+ source_bytes = sequence_source.encode("utf-8") if source_is_text else bytes(sequence_source)
99
+ source_b64 = base64.b64encode(source_bytes).decode("ascii")
91
100
 
92
101
  # 1. Inject the pulseq-bundle.js
93
102
  bundle_placeholder = "<!-- PULSEQ_BUNDLE_PLACEHOLDER -->"
@@ -102,7 +111,9 @@ def _build_html(
102
111
 
103
112
  # 2. Build options injection block (sequence data + display opts)
104
113
  opts = [
105
- f"window.SEQEYES_RAW_B64 = {json.dumps(seq_b64)};",
114
+ f"window.SEQEYES_RAW_B64 = {json.dumps(source_b64)};",
115
+ f"window.SEQEYES_SOURCE_KIND = {json.dumps('text' if source_is_text else 'bytes')};",
116
+ f"window.SEQEYES_SOURCE_NAME = {json.dumps(label)};",
106
117
  f"window.SEQEYES_LABEL = {json.dumps(label)};",
107
118
  f"window.SEQEYES_SHOW_BLOCKS = {json.dumps(show_blocks)};",
108
119
  f"window.SEQEYES_TIME_RANGE = {json.dumps(list(time_range))};",
@@ -138,8 +149,8 @@ class SeqEyesViewer:
138
149
 
139
150
  Parameters
140
151
  ----------
141
- seq_text : str
142
- Raw .seq file content as a string.
152
+ sequence_source : str or bytes-like
153
+ Raw text ``.seq`` content or binary ``.bseq`` bytes.
143
154
  label : str
144
155
  Display label (not yet rendered on the viewer).
145
156
  show_blocks : bool
@@ -150,7 +161,7 @@ class SeqEyesViewer:
150
161
  time_disp : str
151
162
  Time axis unit — ``"s"``, ``"ms"``, or ``"us"``.
152
163
  grad_disp : str
153
- Gradient axis unit — ``"Hz/m"``, ``"kHz/m"``, ``"mT/m"``, ``"G/cm"``.
164
+ Gradient axis unit — ``"Hz/m"``, ``"mT/m"``, or ``"G/cm"``.
154
165
  theme : str or None
155
166
  Colour theme.
156
167
  width : str
@@ -161,33 +172,51 @@ class SeqEyesViewer:
161
172
 
162
173
  def __init__(
163
174
  self,
164
- seq_text: str,
175
+ sequence_source: SequenceSource,
165
176
  *,
166
177
  label: str = "",
167
178
  show_blocks: bool = False,
168
179
  time_range: tuple = (0, float("inf")),
169
180
  time_disp: str = "s",
170
- grad_disp: str = "kHz/m",
181
+ grad_disp: str = "Hz/m",
171
182
  theme: Optional[str] = None,
172
183
  width: str = "100%",
173
184
  height: str = "550px",
174
185
  ) -> None:
175
- self._seq_text = seq_text
186
+ if not isinstance(sequence_source, (str, bytes, bytearray, memoryview)):
187
+ raise TypeError("sequence_source must be .seq text or .bseq bytes")
188
+ self._sequence_source = sequence_source
176
189
  self._label = label
177
190
  self._show_blocks = show_blocks
178
191
  self._time_range = time_range
179
192
  self._time_disp = time_disp
180
- self._grad_disp = grad_disp
193
+ self._grad_disp = _normalize_grad_disp(grad_disp)
181
194
  self._theme = theme
182
195
  self._width = width
183
196
  self._height = height
184
197
 
198
+ @classmethod
199
+ def from_file(cls, path: Union[str, os.PathLike[str]], **kwargs: Any) -> "SeqEyesViewer":
200
+ """Create a viewer from a local ``.seq`` or ``.bseq`` file."""
201
+ source_path = Path(path)
202
+ if source_path.suffix.lower() not in {".seq", ".bseq"}:
203
+ raise ValueError("SeqEyesViewer.from_file() expects a .seq or .bseq file")
204
+ if not source_path.is_file():
205
+ raise FileNotFoundError(source_path)
206
+ kwargs.setdefault("label", source_path.name)
207
+ source: SequenceSource = (
208
+ source_path.read_text(encoding="utf-8")
209
+ if source_path.suffix.lower() == ".seq"
210
+ else source_path.read_bytes()
211
+ )
212
+ return cls(source, **kwargs)
213
+
185
214
  # ── Jupyter integration ───────────────────────────────────────────
186
215
 
187
216
  def _repr_html_(self) -> str:
188
217
  """Return an HTML iframe that Jupyter renders inline."""
189
218
  html = _build_html(
190
- self._seq_text,
219
+ self._sequence_source,
191
220
  theme=self._theme,
192
221
  label=self._label,
193
222
  show_blocks=self._show_blocks,
@@ -225,7 +254,7 @@ class SeqEyesViewer:
225
254
  def to_html(self, *, inject_bundle: bool = True) -> str:
226
255
  """Return the full standalone HTML string (for saving to a file)."""
227
256
  return _build_html(
228
- self._seq_text,
257
+ self._sequence_source,
229
258
  theme=self._theme,
230
259
  inject_bundle=inject_bundle,
231
260
  label=self._label,