figpack 0.2.4__tar.gz → 0.2.6__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.4 → figpack-0.2.6}/MANIFEST.in +1 -1
  2. figpack-0.2.6/PKG-INFO +96 -0
  3. figpack-0.2.6/README.md +43 -0
  4. {figpack-0.2.4 → figpack-0.2.6}/figpack/__init__.py +5 -1
  5. {figpack-0.2.4 → figpack-0.2.6}/figpack/cli.py +2 -118
  6. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/_bundle_utils.py +5 -6
  7. figpack-0.2.6/figpack/core/_save_figure.py +31 -0
  8. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/_server_manager.py +0 -2
  9. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/_show_view.py +22 -22
  10. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/_upload_bundle.py +61 -22
  11. figpack-0.2.6/figpack/core/_view_figure.py +138 -0
  12. figpack-0.2.6/figpack/core/figpack_view.py +160 -0
  13. figpack-0.2.4/figpack/figpack-gui-dist/assets/index-CuFseOGX.js → figpack-0.2.6/figpack/figpack-figure-dist/assets/index-HXdk2TtM.js +58 -58
  14. {figpack-0.2.4/figpack/figpack-gui-dist → figpack-0.2.6/figpack/figpack-figure-dist}/index.html +1 -1
  15. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/Autocorrelograms.py +29 -19
  16. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/CrossCorrelograms.py +29 -19
  17. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/UnitsTable.py +27 -8
  18. figpack-0.2.6/figpack/views/Gallery.py +88 -0
  19. figpack-0.2.6/figpack/views/GalleryItem.py +47 -0
  20. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/Image.py +37 -0
  21. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/Markdown.py +12 -2
  22. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/MatplotlibFigure.py +26 -3
  23. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/PlotlyFigure.py +18 -2
  24. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/__init__.py +2 -0
  25. figpack-0.2.6/figpack.egg-info/PKG-INFO +96 -0
  26. {figpack-0.2.4 → figpack-0.2.6}/figpack.egg-info/SOURCES.txt +15 -5
  27. {figpack-0.2.4 → figpack-0.2.6}/figpack.egg-info/requires.txt +9 -0
  28. {figpack-0.2.4 → figpack-0.2.6}/pyproject.toml +18 -9
  29. {figpack-0.2.4 → figpack-0.2.6}/tests/test_figpack_view.py +0 -14
  30. figpack-0.2.6/tests/test_gallery.py +92 -0
  31. {figpack-0.2.4 → figpack-0.2.6}/tests/test_markdown.py +40 -3
  32. {figpack-0.2.4 → figpack-0.2.6}/tests/test_matplotlib_figure.py +34 -4
  33. {figpack-0.2.4 → figpack-0.2.6}/tests/test_plotly_figure.py +28 -6
  34. figpack-0.2.6/tests/test_raster_plot.py +75 -0
  35. figpack-0.2.6/tests/test_server_manager.py +133 -0
  36. figpack-0.2.6/tests/test_spike_amplitudes.py +110 -0
  37. {figpack-0.2.4 → figpack-0.2.6}/tests/test_spike_sorting_correlograms.py +38 -11
  38. {figpack-0.2.4 → figpack-0.2.6}/tests/test_units_table.py +51 -14
  39. figpack-0.2.6/tests/test_upload_bundle.py +305 -0
  40. figpack-0.2.6/tests/test_view_figure.py +116 -0
  41. figpack-0.2.4/PKG-INFO +0 -168
  42. figpack-0.2.4/README.md +0 -123
  43. figpack-0.2.4/figpack/core/figpack_view.py +0 -111
  44. figpack-0.2.4/figpack.egg-info/PKG-INFO +0 -168
  45. {figpack-0.2.4 → figpack-0.2.6}/LICENSE +0 -0
  46. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/__init__.py +0 -0
  47. {figpack-0.2.4 → figpack-0.2.6}/figpack/core/config.py +0 -0
  48. {figpack-0.2.4/figpack/figpack-gui-dist → figpack-0.2.6/figpack/figpack-figure-dist}/assets/index-Cmae55E4.css +0 -0
  49. {figpack-0.2.4/figpack/figpack-gui-dist → figpack-0.2.6/figpack/figpack-figure-dist}/assets/neurosift-logo-CLsuwLMO.png +0 -0
  50. {figpack-0.2.4 → figpack-0.2.6}/figpack/franklab/__init__.py +0 -0
  51. {figpack-0.2.4 → figpack-0.2.6}/figpack/franklab/views/TrackAnimation.py +0 -0
  52. {figpack-0.2.4 → figpack-0.2.6}/figpack/franklab/views/__init__.py +0 -0
  53. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/__init__.py +0 -0
  54. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
  55. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/AverageWaveforms.py +0 -0
  56. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
  57. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/RasterPlot.py +0 -0
  58. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/RasterPlotItem.py +0 -0
  59. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/SpikeAmplitudes.py +0 -0
  60. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/SpikeAmplitudesItem.py +0 -0
  61. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
  62. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
  63. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
  64. {figpack-0.2.4 → figpack-0.2.6}/figpack/spike_sorting/views/__init__.py +0 -0
  65. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/Box.py +0 -0
  66. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/LayoutItem.py +0 -0
  67. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/MultiChannelTimeseries.py +0 -0
  68. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/Splitter.py +0 -0
  69. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/TabLayout.py +0 -0
  70. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/TabLayoutItem.py +0 -0
  71. {figpack-0.2.4 → figpack-0.2.6}/figpack/views/TimeseriesGraph.py +0 -0
  72. {figpack-0.2.4 → figpack-0.2.6}/figpack.egg-info/dependency_links.txt +0 -0
  73. {figpack-0.2.4 → figpack-0.2.6}/figpack.egg-info/entry_points.txt +0 -0
  74. {figpack-0.2.4 → figpack-0.2.6}/figpack.egg-info/top_level.txt +0 -0
  75. {figpack-0.2.4 → figpack-0.2.6}/setup.cfg +0 -0
  76. {figpack-0.2.4 → figpack-0.2.6}/tests/test_average_waveforms.py +0 -0
  77. {figpack-0.2.4 → figpack-0.2.6}/tests/test_box.py +0 -0
  78. {figpack-0.2.4 → figpack-0.2.6}/tests/test_cli.py +0 -0
  79. {figpack-0.2.4 → figpack-0.2.6}/tests/test_core.py +0 -0
  80. {figpack-0.2.4 → figpack-0.2.6}/tests/test_image.py +0 -0
  81. {figpack-0.2.4 → figpack-0.2.6}/tests/test_multichannel_timeseries.py +0 -0
  82. {figpack-0.2.4 → figpack-0.2.6}/tests/test_show_view.py +0 -0
  83. {figpack-0.2.4 → figpack-0.2.6}/tests/test_splitter.py +0 -0
  84. {figpack-0.2.4 → figpack-0.2.6}/tests/test_tablayout.py +0 -0
  85. {figpack-0.2.4 → figpack-0.2.6}/tests/test_timeseries_graph.py +0 -0
  86. {figpack-0.2.4 → figpack-0.2.6}/tests/test_track_animation.py +0 -0
@@ -2,7 +2,7 @@ include README.md
2
2
  include LICENSE
3
3
  include pyproject.toml
4
4
  recursive-include figpack *.py
5
- recursive-include figpack/figpack-gui-dist *
5
+ recursive-include figpack/figpack-figure-dist *
6
6
  global-exclude __pycache__
7
7
  global-exclude *.py[co]
8
8
 
figpack-0.2.6/PKG-INFO ADDED
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: figpack
3
+ Version: 0.2.6
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.6"
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,23 +9,23 @@ 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.
16
16
 
17
17
  This function:
18
- 1. Copies all files from the figpack-gui-dist directory to tmpdir
18
+ 1. Copies all files from the figpack-figure-dist directory to tmpdir
19
19
  2. Writes the view data to a zarr group
20
20
  3. Consolidates zarr metadata
21
21
 
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
- html_dir = thisdir / ".." / "figpack-gui-dist"
28
+ html_dir = thisdir / ".." / "figpack-figure-dist"
29
29
  if not os.path.exists(html_dir):
30
30
  raise SystemExit(f"Error: directory not found: {html_dir}")
31
31
 
@@ -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, *, title: 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, title=title)
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), title=title)
@@ -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:
@@ -115,10 +110,14 @@ def _show_view(
115
110
 
116
111
  # Upload the bundle
117
112
  figure_url = _upload_bundle(
118
- tmpdir, api_key, title=title, ephemeral=ephemeral
113
+ tmpdir,
114
+ api_key,
115
+ title=title,
116
+ ephemeral=ephemeral,
117
+ use_consolidated_metadata_only=True,
119
118
  )
120
119
 
121
- if use_inline:
120
+ if inline:
122
121
  # For uploaded figures, display the remote URL inline and continue
123
122
  _display_inline_iframe(figure_url, inline_height)
124
123
  else:
@@ -129,8 +128,9 @@ def _show_view(
129
128
  else:
130
129
  print(f"View the figure at: {figure_url}")
131
130
  # Wait until user presses Enter
132
- input("Press Enter to continue...")
133
131
 
132
+ if wait_for_input:
133
+ input("Press Enter to continue...")
134
134
  return figure_url
135
135
  else:
136
136
  # Local server behavior: use process-level server manager
@@ -155,7 +155,7 @@ def _show_view(
155
155
  figure_subdir_name = figure_dir.name
156
156
  figure_url = f"{base_url}/{figure_subdir_name}"
157
157
 
158
- if use_inline:
158
+ if inline:
159
159
  # Display inline and continue (don't block)
160
160
  _display_inline_iframe(figure_url, inline_height)
161
161
  else:
@@ -165,7 +165,7 @@ def _show_view(
165
165
  print(f"Opening {figure_url} in browser.")
166
166
  else:
167
167
  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
168
 
169
+ if wait_for_input:
170
+ input("Press Enter to continue...")
171
171
  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
+ )
99
+
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
91
118
 
92
- return relative_path
119
+ raise last_exception
93
120
 
94
121
 
95
122
  MAX_WORKERS_FOR_UPLOAD = 16
@@ -226,10 +253,22 @@ def _finalize_figure(figure_url: str, api_key: str) -> dict:
226
253
 
227
254
 
228
255
  def _upload_bundle(
229
- tmpdir: str, api_key: str, title: str = None, ephemeral: bool = False
256
+ tmpdir: str,
257
+ api_key: str,
258
+ title: str = None,
259
+ ephemeral: bool = False,
260
+ use_consolidated_metadata_only: bool = False,
230
261
  ) -> str:
231
262
  """
232
263
  Upload the prepared bundle to the cloud using the new database-driven approach
264
+
265
+ Args:
266
+ tmpdir: Path to the temporary directory containing the bundle
267
+ api_key: API key for authentication
268
+ title: Optional title for the figure
269
+ ephemeral: Whether to create an ephemeral figure
270
+ use_consolidated_metadata_only: If True, excludes individual zarr metadata files
271
+ (.zgroup, .zarray, .zattrs) since they are included in .zmetadata
233
272
  """
234
273
  tmpdir_path = pathlib.Path(tmpdir)
235
274
 
@@ -241,6 +280,10 @@ def _upload_bundle(
241
280
  for file_path in tmpdir_path.rglob("*"):
242
281
  if file_path.is_file():
243
282
  relative_path = file_path.relative_to(tmpdir_path)
283
+ # Skip individual zarr metadata files if using consolidated metadata only
284
+ if use_consolidated_metadata_only:
285
+ if str(relative_path).endswith((".zgroup", ".zarray", ".zattrs")):
286
+ continue
244
287
  all_files.append((str(relative_path), file_path))
245
288
 
246
289
  # Calculate total files and size for metadata
@@ -364,17 +407,13 @@ def _upload_bundle(
364
407
  if "manifest.json" not in signed_urls_map:
365
408
  raise Exception("No signed URL returned for manifest.json")
366
409
 
367
- # Upload manifest using signed URL
368
- upload_response = requests.put(
410
+ # Upload manifest using the same retry function
411
+ _upload_single_file_with_signed_url(
412
+ "manifest.json",
413
+ temp_file_path,
369
414
  signed_urls_map["manifest.json"],
370
- data=manifest_content,
371
- headers={"Content-Type": "application/json"},
415
+ num_retries=4,
372
416
  )
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
417
  finally:
379
418
  # Clean up temporary file
380
419
  temp_file_path.unlink(missing_ok=True)