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.

Files changed (86) hide show
  1. figpack-0.2.5/PKG-INFO +96 -0
  2. figpack-0.2.5/README.md +43 -0
  3. {figpack-0.2.4 → figpack-0.2.5}/figpack/__init__.py +5 -1
  4. {figpack-0.2.4 → figpack-0.2.5}/figpack/cli.py +2 -118
  5. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_bundle_utils.py +3 -4
  6. figpack-0.2.5/figpack/core/_save_figure.py +31 -0
  7. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_server_manager.py +0 -2
  8. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_show_view.py +17 -21
  9. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/_upload_bundle.py +44 -21
  10. figpack-0.2.5/figpack/core/_view_figure.py +138 -0
  11. figpack-0.2.5/figpack/core/figpack_view.py +160 -0
  12. 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
  13. {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/index.html +1 -1
  14. figpack-0.2.5/figpack/views/Gallery.py +88 -0
  15. figpack-0.2.5/figpack/views/GalleryItem.py +47 -0
  16. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Image.py +37 -0
  17. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/__init__.py +2 -0
  18. figpack-0.2.5/figpack.egg-info/PKG-INFO +96 -0
  19. {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/SOURCES.txt +12 -2
  20. {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/requires.txt +9 -0
  21. {figpack-0.2.4 → figpack-0.2.5}/pyproject.toml +15 -6
  22. {figpack-0.2.4 → figpack-0.2.5}/tests/test_figpack_view.py +0 -14
  23. figpack-0.2.5/tests/test_gallery.py +92 -0
  24. figpack-0.2.5/tests/test_raster_plot.py +75 -0
  25. figpack-0.2.5/tests/test_server_manager.py +133 -0
  26. figpack-0.2.5/tests/test_spike_amplitudes.py +110 -0
  27. figpack-0.2.5/tests/test_upload_bundle.py +303 -0
  28. figpack-0.2.5/tests/test_view_figure.py +116 -0
  29. figpack-0.2.4/PKG-INFO +0 -168
  30. figpack-0.2.4/README.md +0 -123
  31. figpack-0.2.4/figpack/core/figpack_view.py +0 -111
  32. figpack-0.2.4/figpack.egg-info/PKG-INFO +0 -168
  33. {figpack-0.2.4 → figpack-0.2.5}/LICENSE +0 -0
  34. {figpack-0.2.4 → figpack-0.2.5}/MANIFEST.in +0 -0
  35. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/__init__.py +0 -0
  36. {figpack-0.2.4 → figpack-0.2.5}/figpack/core/config.py +0 -0
  37. {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/assets/index-Cmae55E4.css +0 -0
  38. {figpack-0.2.4 → figpack-0.2.5}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
  39. {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/__init__.py +0 -0
  40. {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/views/TrackAnimation.py +0 -0
  41. {figpack-0.2.4 → figpack-0.2.5}/figpack/franklab/views/__init__.py +0 -0
  42. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/__init__.py +0 -0
  43. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
  44. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
  45. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/AverageWaveforms.py +0 -0
  46. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
  47. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
  48. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/RasterPlot.py +0 -0
  49. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/RasterPlotItem.py +0 -0
  50. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/SpikeAmplitudes.py +0 -0
  51. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/SpikeAmplitudesItem.py +0 -0
  52. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
  53. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTable.py +0 -0
  54. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
  55. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
  56. {figpack-0.2.4 → figpack-0.2.5}/figpack/spike_sorting/views/__init__.py +0 -0
  57. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Box.py +0 -0
  58. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/LayoutItem.py +0 -0
  59. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Markdown.py +0 -0
  60. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/MatplotlibFigure.py +0 -0
  61. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/MultiChannelTimeseries.py +0 -0
  62. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/PlotlyFigure.py +0 -0
  63. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/Splitter.py +0 -0
  64. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TabLayout.py +0 -0
  65. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TabLayoutItem.py +0 -0
  66. {figpack-0.2.4 → figpack-0.2.5}/figpack/views/TimeseriesGraph.py +0 -0
  67. {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/dependency_links.txt +0 -0
  68. {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/entry_points.txt +0 -0
  69. {figpack-0.2.4 → figpack-0.2.5}/figpack.egg-info/top_level.txt +0 -0
  70. {figpack-0.2.4 → figpack-0.2.5}/setup.cfg +0 -0
  71. {figpack-0.2.4 → figpack-0.2.5}/tests/test_average_waveforms.py +0 -0
  72. {figpack-0.2.4 → figpack-0.2.5}/tests/test_box.py +0 -0
  73. {figpack-0.2.4 → figpack-0.2.5}/tests/test_cli.py +0 -0
  74. {figpack-0.2.4 → figpack-0.2.5}/tests/test_core.py +0 -0
  75. {figpack-0.2.4 → figpack-0.2.5}/tests/test_image.py +0 -0
  76. {figpack-0.2.4 → figpack-0.2.5}/tests/test_markdown.py +0 -0
  77. {figpack-0.2.4 → figpack-0.2.5}/tests/test_matplotlib_figure.py +0 -0
  78. {figpack-0.2.4 → figpack-0.2.5}/tests/test_multichannel_timeseries.py +0 -0
  79. {figpack-0.2.4 → figpack-0.2.5}/tests/test_plotly_figure.py +0 -0
  80. {figpack-0.2.4 → figpack-0.2.5}/tests/test_show_view.py +0 -0
  81. {figpack-0.2.4 → figpack-0.2.5}/tests/test_spike_sorting_correlograms.py +0 -0
  82. {figpack-0.2.4 → figpack-0.2.5}/tests/test_splitter.py +0 -0
  83. {figpack-0.2.4 → figpack-0.2.5}/tests/test_tablayout.py +0 -0
  84. {figpack-0.2.4 → figpack-0.2.5}/tests/test_timeseries_graph.py +0 -0
  85. {figpack-0.2.4 → figpack-0.2.5}/tests/test_track_animation.py +0 -0
  86. {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
+ [![Tests](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml/badge.svg)](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml)
57
+ [![codecov](https://codecov.io/gh/flatironinstitute/figpack/branch/main/graph/badge.svg)](https://codecov.io/gh/flatironinstitute/figpack)
58
+ [![PyPI version](https://badge.fury.io/py/figpack.svg)](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.
@@ -0,0 +1,43 @@
1
+ # figpack
2
+
3
+ [![Tests](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml/badge.svg)](https://github.com/flatironinstitute/figpack/actions/workflows/test.yml)
4
+ [![codecov](https://codecov.io/gh/flatironinstitute/figpack/branch/main/graph/badge.svg)](https://codecov.io/gh/flatironinstitute/figpack)
5
+ [![PyPI version](https://badge.fury.io/py/figpack.svg)](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.
@@ -2,4 +2,8 @@
2
2
  figpack - A Python package for creating shareable, interactive visualizations in the browser
3
3
  """
4
4
 
5
- __version__ = "0.2.4"
5
+ __version__ = "0.2.5"
6
+
7
+ from .cli import view_figure
8
+
9
+ __all__ = ["view_figure"]
@@ -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 http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
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 = None, description: str = None
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: Optional title for the figure
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
- if title is not None:
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 = False,
88
- port: Union[int, None] = None,
89
- allow_origin: Union[str, None] = None,
90
- upload: bool = False,
91
- ephemeral: bool = False,
92
- title: Union[str, None] = None,
93
- description: Union[str, None] = None,
94
- inline: Union[bool, None] = None,
95
- inline_height: int = 600,
96
- _local_figure_name: Union[str, None] = None,
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 use_inline:
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 use_inline:
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
- with open(file_path, "rb") as f:
83
- upload_response = requests.put(
84
- signed_url, data=f, headers={"Content-Type": content_type}
85
- )
90
+ retries_remaining = num_retries
91
+ last_exception = None
86
92
 
87
- if not upload_response.ok:
88
- raise Exception(
89
- f"Failed to upload {relative_path} to signed URL: HTTP {upload_response.status_code}"
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
- return relative_path
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 signed URL
368
- upload_response = requests.put(
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
- data=manifest_content,
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)