figpack 0.2.4__tar.gz → 0.2.5__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.
Potentially problematic release.
This version of figpack might be problematic. Click here for more details.
- figpack-0.2.5/PKG-INFO +96 -0
- figpack-0.2.5/README.md +43 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/__init__.py +5 -1
- {figpack-0.2.4 → figpack-0.2.5}/figpack/cli.py +2 -118
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_bundle_utils.py +3 -4
- figpack-0.2.5/figpack/core/_save_figure.py +31 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_server_manager.py +0 -2
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_show_view.py +17 -21
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_upload_bundle.py +44 -21
- figpack-0.2.5/figpack/core/_view_figure.py +138 -0
- figpack-0.2.5/figpack/core/figpack_view.py +160 -0
- figpack-0.2.4/figpack/figpack-gui-dist/assets/index-CuFseOGX.js → figpack-0.2.5/figpack/figpack-gui-dist/assets/index-CrYQmIda.js +57 -57
- {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/index.html +1 -1
- figpack-0.2.5/figpack/views/Gallery.py +88 -0
- figpack-0.2.5/figpack/views/GalleryItem.py +47 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Image.py +37 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/__init__.py +2 -0
- figpack-0.2.5/figpack.egg-info/PKG-INFO +96 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/SOURCES.txt +12 -2
- {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/requires.txt +9 -0
- {figpack-0.2.4 → figpack-0.2.5}/pyproject.toml +15 -6
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_figpack_view.py +0 -14
- figpack-0.2.5/tests/test_gallery.py +92 -0
- figpack-0.2.5/tests/test_raster_plot.py +75 -0
- figpack-0.2.5/tests/test_server_manager.py +133 -0
- figpack-0.2.5/tests/test_spike_amplitudes.py +110 -0
- figpack-0.2.5/tests/test_upload_bundle.py +303 -0
- figpack-0.2.5/tests/test_view_figure.py +116 -0
- figpack-0.2.4/PKG-INFO +0 -168
- figpack-0.2.4/README.md +0 -123
- figpack-0.2.4/figpack/core/figpack_view.py +0 -111
- figpack-0.2.4/figpack.egg-info/PKG-INFO +0 -168
- {figpack-0.2.4 → figpack-0.2.5}/LICENSE +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/MANIFEST.in +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/__init__.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/core/config.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/assets/index-Cmae55E4.css +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/__init__.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/views/TrackAnimation.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/views/__init__.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/__init__.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/AverageWaveforms.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/RasterPlot.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/RasterPlotItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/SpikeAmplitudes.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/SpikeAmplitudesItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTable.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/__init__.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Box.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/LayoutItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Markdown.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/MatplotlibFigure.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/MultiChannelTimeseries.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/PlotlyFigure.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Splitter.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TabLayout.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TabLayoutItem.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TimeseriesGraph.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/setup.cfg +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_average_waveforms.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_box.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_cli.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_core.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_image.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_markdown.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_matplotlib_figure.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_multichannel_timeseries.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_plotly_figure.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_show_view.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_spike_sorting_correlograms.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_splitter.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_tablayout.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_timeseries_graph.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_track_animation.py +0 -0
- {figpack-0.2.4 → figpack-0.2.5}/tests/test_units_table.py +0 -0
figpack-0.2.5/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: figpack
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: A Python package for creating shareable, interactive visualizations in the browser
|
|
5
|
+
Author-email: Jeremy Magland <jmagland@flatironinstitute.org>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/flatironinstitute/figpack
|
|
8
|
+
Project-URL: Repository, https://github.com/flatironinstitute/figpack
|
|
9
|
+
Project-URL: Documentation, https://flatironinstitute.github.io/figpack
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/flatironinstitute/figpack/issues
|
|
11
|
+
Keywords: visualization,plotting,timeseries,interactive
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: zarr<3
|
|
29
|
+
Requires-Dist: requests
|
|
30
|
+
Requires-Dist: psutil
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-mock>=3.10; extra == "test"
|
|
35
|
+
Requires-Dist: spikeinterface; extra == "test"
|
|
36
|
+
Requires-Dist: matplotlib; extra == "test"
|
|
37
|
+
Requires-Dist: plotly; extra == "test"
|
|
38
|
+
Requires-Dist: Pillow; extra == "test"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
43
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
45
|
+
Provides-Extra: docs
|
|
46
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
47
|
+
Requires-Dist: myst-parser>=2.0; extra == "docs"
|
|
48
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
49
|
+
Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "docs"
|
|
50
|
+
Requires-Dist: linkify-it-py>=2.0; extra == "docs"
|
|
51
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# figpack
|
|
55
|
+
|
|
56
|
+
[](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml)
|
|
57
|
+
[](https://codecov.io/gh/flatironinstitute/figpack)
|
|
58
|
+
[](https://badge.fury.io/py/figpack)
|
|
59
|
+
|
|
60
|
+
A Python package for creating shareable, interactive visualizations in the browser.
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
For detailed guidance, tutorials, and API reference, visit our **[documentation](https://flatironinstitute.github.io/figpack)**.
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
Want to jump right in? Here's how to get started:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install figpack
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import numpy as np
|
|
76
|
+
import figpack.views as vv
|
|
77
|
+
|
|
78
|
+
# Create a timeseries graph
|
|
79
|
+
graph = vv.TimeseriesGraph(y_label="Signal")
|
|
80
|
+
|
|
81
|
+
# Add some data
|
|
82
|
+
t = np.linspace(0, 10, 1000)
|
|
83
|
+
y = np.sin(2 * np.pi * t)
|
|
84
|
+
graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
|
|
85
|
+
|
|
86
|
+
# Display the visualization in your browser
|
|
87
|
+
graph.show(open_in_browser=True, title="Quick Start Example")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
Apache-2.0
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Visit the [GitHub repository](https://github.com/flatironinstitute/figpack) for issues, contributions, and the latest updates.
|
figpack-0.2.5/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# figpack
|
|
2
|
+
|
|
3
|
+
[](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml)
|
|
4
|
+
[](https://codecov.io/gh/flatironinstitute/figpack)
|
|
5
|
+
[](https://badge.fury.io/py/figpack)
|
|
6
|
+
|
|
7
|
+
A Python package for creating shareable, interactive visualizations in the browser.
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
For detailed guidance, tutorials, and API reference, visit our **[documentation](https://flatironinstitute.github.io/figpack)**.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Want to jump right in? Here's how to get started:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install figpack
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
import figpack.views as vv
|
|
24
|
+
|
|
25
|
+
# Create a timeseries graph
|
|
26
|
+
graph = vv.TimeseriesGraph(y_label="Signal")
|
|
27
|
+
|
|
28
|
+
# Add some data
|
|
29
|
+
t = np.linspace(0, 10, 1000)
|
|
30
|
+
y = np.sin(2 * np.pi * t)
|
|
31
|
+
graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
|
|
32
|
+
|
|
33
|
+
# Display the visualization in your browser
|
|
34
|
+
graph.show(open_in_browser=True, title="Quick Start Example")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
Apache-2.0
|
|
40
|
+
|
|
41
|
+
## Contributing
|
|
42
|
+
|
|
43
|
+
Visit the [GitHub repository](https://github.com/flatironinstitute/figpack) for issues, contributions, and the latest updates.
|
|
@@ -5,21 +5,19 @@ Command-line interface for figpack
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import pathlib
|
|
8
|
-
import socket
|
|
9
8
|
import sys
|
|
10
9
|
import tarfile
|
|
11
10
|
import tempfile
|
|
12
11
|
import threading
|
|
13
|
-
import webbrowser
|
|
14
12
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
|
-
from
|
|
16
|
-
from typing import Dict, Tuple, Union
|
|
13
|
+
from typing import Dict, Tuple
|
|
17
14
|
from urllib.parse import urljoin
|
|
18
15
|
|
|
19
16
|
import requests
|
|
20
17
|
|
|
21
18
|
from . import __version__
|
|
22
19
|
from .core._server_manager import CORSRequestHandler
|
|
20
|
+
from .core._view_figure import serve_files, view_figure
|
|
23
21
|
|
|
24
22
|
MAX_WORKERS_FOR_DOWNLOAD = 16
|
|
25
23
|
|
|
@@ -216,120 +214,6 @@ def download_figure(figure_url: str, dest_path: str) -> None:
|
|
|
216
214
|
print(f"Archive saved to: {dest_path}")
|
|
217
215
|
|
|
218
216
|
|
|
219
|
-
def serve_files(
|
|
220
|
-
tmpdir: str,
|
|
221
|
-
*,
|
|
222
|
-
port: Union[int, None],
|
|
223
|
-
open_in_browser: bool = False,
|
|
224
|
-
allow_origin: Union[str, None] = None,
|
|
225
|
-
):
|
|
226
|
-
"""
|
|
227
|
-
Serve files from a directory using a simple HTTP server.
|
|
228
|
-
|
|
229
|
-
Args:
|
|
230
|
-
tmpdir: Directory to serve
|
|
231
|
-
port: Port number for local server
|
|
232
|
-
open_in_browser: Whether to open in browser automatically
|
|
233
|
-
allow_origin: CORS allow origin header
|
|
234
|
-
"""
|
|
235
|
-
# if port is None, find a free port
|
|
236
|
-
if port is None:
|
|
237
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
238
|
-
s.bind(("", 0))
|
|
239
|
-
port = s.getsockname()[1]
|
|
240
|
-
|
|
241
|
-
tmpdir = pathlib.Path(tmpdir)
|
|
242
|
-
tmpdir = tmpdir.resolve()
|
|
243
|
-
if not tmpdir.exists() or not tmpdir.is_dir():
|
|
244
|
-
raise SystemExit(f"Directory not found: {tmpdir}")
|
|
245
|
-
|
|
246
|
-
# Configure handler with directory and allow_origin
|
|
247
|
-
def handler_factory(*args, **kwargs):
|
|
248
|
-
return CORSRequestHandler(
|
|
249
|
-
*args, directory=str(tmpdir), allow_origin=allow_origin, **kwargs
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
|
|
253
|
-
print(f"Serving {tmpdir} at http://localhost:{port} (CORS → {allow_origin})")
|
|
254
|
-
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
255
|
-
thread.start()
|
|
256
|
-
|
|
257
|
-
if open_in_browser:
|
|
258
|
-
webbrowser.open(f"http://localhost:{port}")
|
|
259
|
-
print(f"Opening http://localhost:{port} in your browser.")
|
|
260
|
-
else:
|
|
261
|
-
print(
|
|
262
|
-
f"Open http://localhost:{port} in your browser to view the visualization."
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
try:
|
|
266
|
-
input("Press Enter to stop...\n")
|
|
267
|
-
except (KeyboardInterrupt, EOFError):
|
|
268
|
-
pass
|
|
269
|
-
finally:
|
|
270
|
-
print("Shutting down server...")
|
|
271
|
-
httpd.shutdown()
|
|
272
|
-
httpd.server_close()
|
|
273
|
-
thread.join()
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def view_figure(archive_path: str, port: Union[int, None] = None) -> None:
|
|
277
|
-
"""
|
|
278
|
-
Extract and serve a figure archive locally
|
|
279
|
-
|
|
280
|
-
Args:
|
|
281
|
-
archive_path: Path to the tar.gz archive
|
|
282
|
-
port: Optional port number to serve on
|
|
283
|
-
"""
|
|
284
|
-
archive_pathlib = pathlib.Path(archive_path)
|
|
285
|
-
|
|
286
|
-
if not archive_pathlib.exists():
|
|
287
|
-
print(f"Error: Archive file not found: {archive_path}")
|
|
288
|
-
sys.exit(1)
|
|
289
|
-
|
|
290
|
-
if not archive_pathlib.suffix.lower() in [".gz", ".tgz"] or not str(
|
|
291
|
-
archive_pathlib
|
|
292
|
-
).endswith(".tar.gz"):
|
|
293
|
-
print(f"Error: Expected a .tar.gz file, got: {archive_path}")
|
|
294
|
-
sys.exit(1)
|
|
295
|
-
|
|
296
|
-
print(f"Extracting figure archive: {archive_path}")
|
|
297
|
-
|
|
298
|
-
# Create temporary directory and extract files
|
|
299
|
-
with tempfile.TemporaryDirectory(prefix="figpack_view_") as temp_dir:
|
|
300
|
-
temp_path = pathlib.Path(temp_dir)
|
|
301
|
-
|
|
302
|
-
try:
|
|
303
|
-
with tarfile.open(archive_path, "r:gz") as tar:
|
|
304
|
-
tar.extractall(temp_path)
|
|
305
|
-
|
|
306
|
-
# Count extracted files
|
|
307
|
-
extracted_files = list(temp_path.rglob("*"))
|
|
308
|
-
file_count = len([f for f in extracted_files if f.is_file()])
|
|
309
|
-
print(f"Extracted {file_count} files")
|
|
310
|
-
|
|
311
|
-
# Check if index.html exists
|
|
312
|
-
index_html = temp_path / "index.html"
|
|
313
|
-
if not index_html.exists():
|
|
314
|
-
print("Warning: No index.html found in archive")
|
|
315
|
-
print("Available files:")
|
|
316
|
-
for f in sorted(extracted_files):
|
|
317
|
-
if f.is_file():
|
|
318
|
-
print(f" {f.relative_to(temp_path)}")
|
|
319
|
-
|
|
320
|
-
# Serve the files
|
|
321
|
-
serve_files(
|
|
322
|
-
str(temp_path),
|
|
323
|
-
port=port,
|
|
324
|
-
open_in_browser=True,
|
|
325
|
-
allow_origin=None,
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
except tarfile.TarError as e:
|
|
329
|
-
print(f"Error: Failed to extract archive: {e}")
|
|
330
|
-
sys.exit(1)
|
|
331
|
-
|
|
332
|
-
|
|
333
217
|
def main():
|
|
334
218
|
"""Main CLI entry point"""
|
|
335
219
|
parser = argparse.ArgumentParser(
|
|
@@ -9,7 +9,7 @@ thisdir = pathlib.Path(__file__).parent.resolve()
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def prepare_figure_bundle(
|
|
12
|
-
view: FigpackView, tmpdir: str, *, title: str
|
|
12
|
+
view: FigpackView, tmpdir: str, *, title: str, description: str = None
|
|
13
13
|
) -> None:
|
|
14
14
|
"""
|
|
15
15
|
Prepare a figure bundle in the specified temporary directory.
|
|
@@ -22,7 +22,7 @@ def prepare_figure_bundle(
|
|
|
22
22
|
Args:
|
|
23
23
|
view: The figpack view to prepare
|
|
24
24
|
tmpdir: The temporary directory to prepare the bundle in
|
|
25
|
-
title:
|
|
25
|
+
title: Title for the figure (required)
|
|
26
26
|
description: Optional description for the figure (markdown supported)
|
|
27
27
|
"""
|
|
28
28
|
html_dir = thisdir / ".." / "figpack-gui-dist"
|
|
@@ -50,8 +50,7 @@ def prepare_figure_bundle(
|
|
|
50
50
|
view._write_to_zarr_group(zarr_group)
|
|
51
51
|
|
|
52
52
|
# Add title and description as attributes on the top-level zarr group
|
|
53
|
-
|
|
54
|
-
zarr_group.attrs["title"] = title
|
|
53
|
+
zarr_group.attrs["title"] = title
|
|
55
54
|
if description is not None:
|
|
56
55
|
zarr_group.attrs["description"] = description
|
|
57
56
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
from ._bundle_utils import prepare_figure_bundle
|
|
5
|
+
from .figpack_view import FigpackView
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _save_figure(view: FigpackView, output_path: str):
|
|
9
|
+
"""
|
|
10
|
+
Save the figure to a folder or a .tar.gz file
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
view: FigpackView instance to save
|
|
14
|
+
output_path: Output path (destination folder or .tar.gz file path)
|
|
15
|
+
"""
|
|
16
|
+
output_path = pathlib.Path(output_path)
|
|
17
|
+
if (output_path.suffix == ".gz" and output_path.suffixes[-2] == ".tar") or (
|
|
18
|
+
output_path.suffix == ".tgz"
|
|
19
|
+
):
|
|
20
|
+
# It's a .tar.gz file
|
|
21
|
+
with tempfile.TemporaryDirectory(prefix="figpack_save_") as tmpdir:
|
|
22
|
+
prepare_figure_bundle(view, tmpdir)
|
|
23
|
+
# Create tar.gz file
|
|
24
|
+
import tarfile
|
|
25
|
+
|
|
26
|
+
with tarfile.open(output_path, "w:gz") as tar:
|
|
27
|
+
tar.add(tmpdir, arcname=".")
|
|
28
|
+
else:
|
|
29
|
+
# It's a folder
|
|
30
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
prepare_figure_bundle(view, str(output_path))
|
|
@@ -206,8 +206,6 @@ class ProcessServerManager:
|
|
|
206
206
|
# Start directory monitoring thread
|
|
207
207
|
self._start_directory_monitor()
|
|
208
208
|
|
|
209
|
-
print(f"Started figpack server at http://localhost:{port} serving {temp_dir}")
|
|
210
|
-
|
|
211
209
|
return f"http://localhost:{port}", port
|
|
212
210
|
|
|
213
211
|
def _stop_server(self):
|
|
@@ -84,23 +84,18 @@ thisdir = pathlib.Path(__file__).parent.resolve()
|
|
|
84
84
|
def _show_view(
|
|
85
85
|
view: FigpackView,
|
|
86
86
|
*,
|
|
87
|
-
open_in_browser: bool
|
|
88
|
-
port: Union[int, None]
|
|
89
|
-
allow_origin: Union[str, None]
|
|
90
|
-
upload: bool
|
|
91
|
-
ephemeral: bool
|
|
92
|
-
title:
|
|
93
|
-
description: Union[str, None]
|
|
94
|
-
inline:
|
|
95
|
-
inline_height: int
|
|
96
|
-
|
|
87
|
+
open_in_browser: bool,
|
|
88
|
+
port: Union[int, None],
|
|
89
|
+
allow_origin: Union[str, None],
|
|
90
|
+
upload: bool,
|
|
91
|
+
ephemeral: bool,
|
|
92
|
+
title: str,
|
|
93
|
+
description: Union[str, None],
|
|
94
|
+
inline: bool,
|
|
95
|
+
inline_height: int,
|
|
96
|
+
wait_for_input: bool,
|
|
97
|
+
_local_figure_name: Union[str, None],
|
|
97
98
|
):
|
|
98
|
-
# Determine if we should use inline display
|
|
99
|
-
use_inline = inline
|
|
100
|
-
if inline is None:
|
|
101
|
-
# Auto-detect: use inline if we're in a notebook
|
|
102
|
-
use_inline = _is_in_notebook()
|
|
103
|
-
|
|
104
99
|
if upload:
|
|
105
100
|
# Upload behavior: create temporary directory for this upload only
|
|
106
101
|
with tempfile.TemporaryDirectory(prefix="figpack_upload_") as tmpdir:
|
|
@@ -118,7 +113,7 @@ def _show_view(
|
|
|
118
113
|
tmpdir, api_key, title=title, ephemeral=ephemeral
|
|
119
114
|
)
|
|
120
115
|
|
|
121
|
-
if
|
|
116
|
+
if inline:
|
|
122
117
|
# For uploaded figures, display the remote URL inline and continue
|
|
123
118
|
_display_inline_iframe(figure_url, inline_height)
|
|
124
119
|
else:
|
|
@@ -129,8 +124,9 @@ def _show_view(
|
|
|
129
124
|
else:
|
|
130
125
|
print(f"View the figure at: {figure_url}")
|
|
131
126
|
# Wait until user presses Enter
|
|
132
|
-
input("Press Enter to continue...")
|
|
133
127
|
|
|
128
|
+
if wait_for_input:
|
|
129
|
+
input("Press Enter to continue...")
|
|
134
130
|
return figure_url
|
|
135
131
|
else:
|
|
136
132
|
# Local server behavior: use process-level server manager
|
|
@@ -155,7 +151,7 @@ def _show_view(
|
|
|
155
151
|
figure_subdir_name = figure_dir.name
|
|
156
152
|
figure_url = f"{base_url}/{figure_subdir_name}"
|
|
157
153
|
|
|
158
|
-
if
|
|
154
|
+
if inline:
|
|
159
155
|
# Display inline and continue (don't block)
|
|
160
156
|
_display_inline_iframe(figure_url, inline_height)
|
|
161
157
|
else:
|
|
@@ -165,7 +161,7 @@ def _show_view(
|
|
|
165
161
|
print(f"Opening {figure_url} in browser.")
|
|
166
162
|
else:
|
|
167
163
|
print(f"Open {figure_url} in your browser to view the visualization.")
|
|
168
|
-
# Wait until user presses Enter
|
|
169
|
-
input("Press Enter to continue...")
|
|
170
164
|
|
|
165
|
+
if wait_for_input:
|
|
166
|
+
input("Press Enter to continue...")
|
|
171
167
|
return figure_url
|
|
@@ -69,27 +69,54 @@ def _get_batch_signed_urls(figure_url: str, files_batch: list, api_key: str) ->
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def _upload_single_file_with_signed_url(
|
|
72
|
-
relative_path: str, file_path: pathlib.Path, signed_url: str
|
|
72
|
+
relative_path: str, file_path: pathlib.Path, signed_url: str, num_retries: int = 4
|
|
73
73
|
) -> str:
|
|
74
74
|
"""
|
|
75
|
-
Upload a single file using a pre-obtained signed URL
|
|
75
|
+
Upload a single file using a pre-obtained signed URL with exponential backoff retries
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
relative_path: The relative path of the file
|
|
79
|
+
file_path: The path to the file to upload
|
|
80
|
+
signed_url: The signed URL to upload to
|
|
81
|
+
num_retries: Number of retries on failure with exponential backoff (default: 4)
|
|
76
82
|
|
|
77
83
|
Returns:
|
|
78
84
|
str: The relative path of the uploaded file
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
Exception: If upload fails after all retries are exhausted
|
|
79
88
|
"""
|
|
80
|
-
# Upload file to signed URL
|
|
81
89
|
content_type = _determine_content_type(relative_path)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
signed_url, data=f, headers={"Content-Type": content_type}
|
|
85
|
-
)
|
|
90
|
+
retries_remaining = num_retries
|
|
91
|
+
last_exception = None
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
while retries_remaining >= 0:
|
|
94
|
+
try:
|
|
95
|
+
with open(file_path, "rb") as f:
|
|
96
|
+
upload_response = requests.put(
|
|
97
|
+
signed_url, data=f, headers={"Content-Type": content_type}
|
|
98
|
+
)
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
if upload_response.ok:
|
|
101
|
+
return relative_path
|
|
102
|
+
|
|
103
|
+
last_exception = Exception(
|
|
104
|
+
f"Failed to upload {relative_path} to signed URL: HTTP {upload_response.status_code}"
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
last_exception = e
|
|
108
|
+
|
|
109
|
+
if retries_remaining > 0:
|
|
110
|
+
backoff_seconds = 2 ** (num_retries - retries_remaining)
|
|
111
|
+
print(
|
|
112
|
+
f"Upload failed for {relative_path}, retrying in {backoff_seconds} seconds..."
|
|
113
|
+
)
|
|
114
|
+
time.sleep(backoff_seconds)
|
|
115
|
+
retries_remaining -= 1
|
|
116
|
+
else:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
raise last_exception
|
|
93
120
|
|
|
94
121
|
|
|
95
122
|
MAX_WORKERS_FOR_UPLOAD = 16
|
|
@@ -364,17 +391,13 @@ def _upload_bundle(
|
|
|
364
391
|
if "manifest.json" not in signed_urls_map:
|
|
365
392
|
raise Exception("No signed URL returned for manifest.json")
|
|
366
393
|
|
|
367
|
-
# Upload manifest using
|
|
368
|
-
|
|
394
|
+
# Upload manifest using the same retry function
|
|
395
|
+
_upload_single_file_with_signed_url(
|
|
396
|
+
"manifest.json",
|
|
397
|
+
temp_file_path,
|
|
369
398
|
signed_urls_map["manifest.json"],
|
|
370
|
-
|
|
371
|
-
headers={"Content-Type": "application/json"},
|
|
399
|
+
num_retries=4,
|
|
372
400
|
)
|
|
373
|
-
|
|
374
|
-
if not upload_response.ok:
|
|
375
|
-
raise Exception(
|
|
376
|
-
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
377
|
-
)
|
|
378
401
|
finally:
|
|
379
402
|
# Clean up temporary file
|
|
380
403
|
temp_file_path.unlink(missing_ok=True)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core functionality for viewing figures locally
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
import socket
|
|
7
|
+
import sys
|
|
8
|
+
import tarfile
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import webbrowser
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from ._server_manager import CORSRequestHandler, ThreadingHTTPServer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def serve_files(
|
|
18
|
+
tmpdir: str,
|
|
19
|
+
*,
|
|
20
|
+
port: Union[int, None],
|
|
21
|
+
open_in_browser: bool = False,
|
|
22
|
+
allow_origin: Union[str, None] = None,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Serve files from a directory using a simple HTTP server.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
tmpdir: Directory to serve
|
|
29
|
+
port: Port number for local server
|
|
30
|
+
open_in_browser: Whether to open in browser automatically
|
|
31
|
+
allow_origin: CORS allow origin header
|
|
32
|
+
"""
|
|
33
|
+
# if port is None, find a free port
|
|
34
|
+
if port is None:
|
|
35
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
36
|
+
s.bind(("", 0))
|
|
37
|
+
port = s.getsockname()[1]
|
|
38
|
+
|
|
39
|
+
tmpdir = pathlib.Path(tmpdir)
|
|
40
|
+
tmpdir = tmpdir.resolve()
|
|
41
|
+
if not tmpdir.exists() or not tmpdir.is_dir():
|
|
42
|
+
raise SystemExit(f"Directory not found: {tmpdir}")
|
|
43
|
+
|
|
44
|
+
# Configure handler with directory and allow_origin
|
|
45
|
+
def handler_factory(*args, **kwargs):
|
|
46
|
+
return CORSRequestHandler(
|
|
47
|
+
*args, directory=str(tmpdir), allow_origin=allow_origin, **kwargs
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
|
|
51
|
+
print(f"Serving {tmpdir} at http://localhost:{port} (CORS → {allow_origin})")
|
|
52
|
+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
53
|
+
thread.start()
|
|
54
|
+
|
|
55
|
+
if open_in_browser:
|
|
56
|
+
webbrowser.open(f"http://localhost:{port}")
|
|
57
|
+
print(f"Opening http://localhost:{port} in your browser.")
|
|
58
|
+
else:
|
|
59
|
+
print(
|
|
60
|
+
f"Open http://localhost:{port} in your browser to view the visualization."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
input("Press Enter to stop...\n")
|
|
65
|
+
except (KeyboardInterrupt, EOFError):
|
|
66
|
+
pass
|
|
67
|
+
finally:
|
|
68
|
+
print("Shutting down server...")
|
|
69
|
+
httpd.shutdown()
|
|
70
|
+
httpd.server_close()
|
|
71
|
+
thread.join()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def view_figure(figure_path: str, port: Union[int, None] = None) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Extract and serve a figure archive locally
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
figure_path: Path to a .tar.gz archive file or a directory
|
|
80
|
+
port: Optional port number to serve on
|
|
81
|
+
"""
|
|
82
|
+
figure_pathlib = pathlib.Path(figure_path)
|
|
83
|
+
|
|
84
|
+
if not figure_pathlib.exists():
|
|
85
|
+
print(f"Error: Archive file not found: {figure_path}")
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
if figure_pathlib.is_dir():
|
|
89
|
+
# We assume it's a directory
|
|
90
|
+
serve_files(
|
|
91
|
+
str(figure_pathlib),
|
|
92
|
+
port=port,
|
|
93
|
+
open_in_browser=True,
|
|
94
|
+
allow_origin=None,
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if not figure_pathlib.suffix.lower() in [".gz", ".tgz"] or not str(
|
|
99
|
+
figure_pathlib
|
|
100
|
+
).endswith(".tar.gz"):
|
|
101
|
+
print(f"Error: Archive file must be a .tar.gz file: {figure_path}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
print(f"Extracting figure archive: {figure_path}")
|
|
105
|
+
|
|
106
|
+
# Create temporary directory and extract files
|
|
107
|
+
with tempfile.TemporaryDirectory(prefix="figpack_view_") as temp_dir:
|
|
108
|
+
temp_path = pathlib.Path(temp_dir)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
with tarfile.open(figure_path, "r:gz") as tar:
|
|
112
|
+
tar.extractall(temp_path)
|
|
113
|
+
|
|
114
|
+
# Count extracted files
|
|
115
|
+
extracted_files = list(temp_path.rglob("*"))
|
|
116
|
+
file_count = len([f for f in extracted_files if f.is_file()])
|
|
117
|
+
print(f"Extracted {file_count} files")
|
|
118
|
+
|
|
119
|
+
# Check if index.html exists
|
|
120
|
+
index_html = temp_path / "index.html"
|
|
121
|
+
if not index_html.exists():
|
|
122
|
+
print("Warning: No index.html found in archive")
|
|
123
|
+
print("Available files:")
|
|
124
|
+
for f in sorted(extracted_files):
|
|
125
|
+
if f.is_file():
|
|
126
|
+
print(f" {f.relative_to(temp_path)}")
|
|
127
|
+
|
|
128
|
+
# Serve the files
|
|
129
|
+
serve_files(
|
|
130
|
+
str(temp_path),
|
|
131
|
+
port=port,
|
|
132
|
+
open_in_browser=True,
|
|
133
|
+
allow_origin=None,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except tarfile.TarError as e:
|
|
137
|
+
print(f"Error: Failed to extract archive: {e}")
|
|
138
|
+
sys.exit(1)
|