figpack 0.1.4__tar.gz → 0.1.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 (67) hide show
  1. {figpack-0.1.4/figpack.egg-info → figpack-0.1.6}/PKG-INFO +49 -8
  2. figpack-0.1.6/README.md +123 -0
  3. figpack-0.1.6/figpack/__init__.py +5 -0
  4. {figpack-0.1.4 → figpack-0.1.6}/figpack/cli.py +8 -7
  5. {figpack-0.1.4 → figpack-0.1.6}/figpack/core/_bundle_utils.py +2 -0
  6. {figpack-0.1.4 → figpack-0.1.6}/figpack/core/_show_view.py +8 -12
  7. figpack-0.1.6/figpack/core/_upload_bundle.py +343 -0
  8. figpack-0.1.6/figpack/core/config.py +1 -0
  9. {figpack-0.1.4 → figpack-0.1.6}/figpack/core/figpack_view.py +1 -0
  10. figpack-0.1.4/figpack/figpack-gui-dist/assets/index-Dw14QqeQ.js → figpack-0.1.6/figpack/figpack-gui-dist/assets/index-DaeClgi6.js +91 -91
  11. {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/index.html +1 -1
  12. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/AutocorrelogramItem.py +1 -0
  13. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/Autocorrelograms.py +39 -9
  14. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelogramItem.py +1 -0
  15. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/CrossCorrelograms.py +45 -7
  16. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTable.py +5 -3
  17. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableRow.py +1 -1
  18. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Box.py +9 -1
  19. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Image.py +12 -2
  20. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/LayoutItem.py +1 -0
  21. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Markdown.py +1 -0
  22. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/MatplotlibFigure.py +4 -2
  23. figpack-0.1.6/figpack/views/MultiChannelTimeseries.py +230 -0
  24. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/PlotlyFigure.py +5 -3
  25. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/Splitter.py +3 -1
  26. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TabLayout.py +3 -1
  27. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TabLayoutItem.py +1 -0
  28. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/TimeseriesGraph.py +3 -2
  29. {figpack-0.1.4 → figpack-0.1.6}/figpack/views/__init__.py +7 -6
  30. {figpack-0.1.4 → figpack-0.1.6/figpack.egg-info}/PKG-INFO +49 -8
  31. {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/SOURCES.txt +20 -2
  32. figpack-0.1.6/figpack.egg-info/requires.txt +18 -0
  33. figpack-0.1.6/pyproject.toml +130 -0
  34. figpack-0.1.6/tests/test_box.py +62 -0
  35. figpack-0.1.6/tests/test_cli.py +275 -0
  36. figpack-0.1.6/tests/test_core.py +62 -0
  37. figpack-0.1.6/tests/test_figpack_view.py +106 -0
  38. figpack-0.1.6/tests/test_image.py +60 -0
  39. figpack-0.1.6/tests/test_markdown.py +65 -0
  40. figpack-0.1.6/tests/test_matplotlib_figure.py +111 -0
  41. figpack-0.1.6/tests/test_multichannel_timeseries.py +221 -0
  42. figpack-0.1.6/tests/test_plotly_figure.py +124 -0
  43. figpack-0.1.6/tests/test_show_view.py +521 -0
  44. figpack-0.1.6/tests/test_spike_sorting_correlograms.py +133 -0
  45. figpack-0.1.6/tests/test_splitter.py +108 -0
  46. figpack-0.1.6/tests/test_tablayout.py +150 -0
  47. figpack-0.1.6/tests/test_timeseries_graph.py +275 -0
  48. figpack-0.1.6/tests/test_units_table.py +172 -0
  49. figpack-0.1.6/tests/test_upload_bundle.py +679 -0
  50. figpack-0.1.4/README.md +0 -95
  51. figpack-0.1.4/figpack/__init__.py +0 -9
  52. figpack-0.1.4/figpack/core/_upload_bundle.py +0 -451
  53. figpack-0.1.4/figpack.egg-info/requires.txt +0 -3
  54. figpack-0.1.4/pyproject.toml +0 -48
  55. {figpack-0.1.4 → figpack-0.1.6}/LICENSE +0 -0
  56. {figpack-0.1.4 → figpack-0.1.6}/MANIFEST.in +0 -0
  57. {figpack-0.1.4 → figpack-0.1.6}/figpack/core/__init__.py +0 -0
  58. {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -0
  59. {figpack-0.1.4 → figpack-0.1.6}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
  60. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/__init__.py +0 -0
  61. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
  62. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
  63. {figpack-0.1.4 → figpack-0.1.6}/figpack/spike_sorting/views/__init__.py +2 -2
  64. {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/dependency_links.txt +0 -0
  65. {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/entry_points.txt +0 -0
  66. {figpack-0.1.4 → figpack-0.1.6}/figpack.egg-info/top_level.txt +0 -0
  67. {figpack-0.1.4 → figpack-0.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: figpack
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A Python package for creating shareable, interactive visualizations in the browser
5
5
  Author-email: Jeremy Magland <jmagland@flatironinstitute.org>
6
6
  License: Apache-2.0
@@ -27,6 +27,19 @@ License-File: LICENSE
27
27
  Requires-Dist: numpy
28
28
  Requires-Dist: zarr
29
29
  Requires-Dist: requests
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=7.0; extra == "test"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
33
+ Requires-Dist: pytest-mock>=3.10; extra == "test"
34
+ Requires-Dist: spikeinterface; extra == "test"
35
+ Requires-Dist: matplotlib; extra == "test"
36
+ Requires-Dist: plotly; extra == "test"
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7.0; extra == "dev"
39
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
40
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
41
+ Requires-Dist: black>=24.0; extra == "dev"
42
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
30
43
  Dynamic: license-file
31
44
 
32
45
  # figpack
@@ -68,32 +81,60 @@ y = np.sin(2 * np.pi * t)
68
81
  graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
69
82
 
70
83
  # Display the visualization
71
- graph.show(open_in_browser=True)
84
+ graph.show(open_in_browser=True, title="Quick Start Example")
72
85
  ```
73
86
 
87
+ ## Available Views
88
+
89
+ figpack provides a comprehensive set of view components for creating interactive visualizations:
90
+
91
+ ### Core Views
92
+
93
+ - **[TimeseriesGraph](docs/timeseries-graph.md)** - Interactive line plots, markers, and intervals ([example](examples/example_timeseries_graph.py))
94
+ - **[MultiChannelTimeseries](docs/multichannel-timeseries.md)** - Multi-channel timeseries visualization ([example](examples/example_multichannel_timeseries.py))
95
+ - **[Image](docs/image.md)** - Display images with optional annotations ([example](examples/example_image.py))
96
+ - **[Markdown](docs/markdown.md)** - Render markdown content ([example](examples/example_markdown.py))
97
+
98
+ ### Layout Views
99
+
100
+ - **[Box](docs/box.md)** - Flexible container with horizontal/vertical layouts ([example](examples/example_box.py))
101
+ - **[Splitter](docs/splitter.md)** - Resizable split panes ([example](examples/example_splitter.py))
102
+ - **[TabLayout](docs/tab-layout.md)** - Tabbed interface for multiple views ([example](examples/example_tablayout.py))
103
+
104
+ ### External Figure Support
105
+
106
+ - **[MatplotlibFigure](docs/matplotlib-figure.md)** - Embed matplotlib plots ([example](examples/example_matplotlib.py))
107
+ - **[PlotlyFigure](docs/plotly-figure.md)** - Embed plotly visualizations ([example](examples/example_plotly.py))
108
+
109
+ ### Spike Sorting Views
110
+
111
+ - **[Autocorrelograms](docs/autocorrelograms.md)** - Auto-correlation analysis ([example](examples/example_autocorrelograms.py))
112
+ - **[CrossCorrelograms](docs/cross-correlograms.md)** - Cross-correlation analysis ([example](examples/example_cross_correlograms.py))
113
+ - **[UnitsTable](docs/units-table.md)** - Sortable table for spike sorting units ([example](examples/example_units_table.py))
114
+
74
115
  ## Examples
75
116
 
76
- See the `examples/` directory.
117
+ See the `examples/` directory for working examples of each view type.
77
118
 
78
119
  ## Usage Modes
79
120
 
80
- ### Local Development
121
+ ### Local-only Mode
81
122
 
82
123
  ```python
83
- view.show(open_in_browser=True)
124
+ view.show(open_in_browser=True, title="Local Visualization")
84
125
  ```
85
126
 
86
127
  ### Sharing Online
87
128
 
88
- Set the `FIGPACK_UPLOAD_PASSCODE` environment variable and use:
129
+ Set the `FIGPACK_API_KEY` environment variable and use:
89
130
 
90
131
  ```python
91
- view.show(upload=True, open_in_browser=True)
132
+ view.show(upload=True, open_in_browser=True, title="Shared Visualization")
92
133
  ```
93
134
 
94
135
  ### Development Mode
95
136
 
96
- Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and debugging with figpack-gui.
137
+ Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and development of figpack-gui.
97
138
 
98
139
  ## Command Line Interface
99
140
 
@@ -0,0 +1,123 @@
1
+ # figpack
2
+
3
+ A Python package for creating shareable, interactive visualizations in the browser.
4
+
5
+ ## Overview
6
+
7
+ figpack enables you to create interactive data visualizations that can be displayed in a web browser and optionally shared online. The package focuses on timeseries data visualization with support for complex, nested layouts.
8
+
9
+ ### Key Features
10
+
11
+ - **Interactive timeseries graphs** with line series, markers, and interval plots
12
+ - **Flexible layout system** with boxes, splitters, and tab layouts
13
+ - **Web-based rendering** that works in any modern browser
14
+ - **Shareable visualizations** that can be uploaded and shared via URLs
15
+ - **Zarr-based data storage** for efficient handling of large datasets
16
+
17
+ ## Installation
18
+
19
+ Install figpack using pip:
20
+
21
+ ```bash
22
+ pip install figpack
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ import numpy as np
29
+ import figpack.views as vv
30
+
31
+ # Create a timeseries graph
32
+ graph = vv.TimeseriesGraph(y_label="Signal")
33
+
34
+ # Add some data
35
+ t = np.linspace(0, 10, 1000)
36
+ y = np.sin(2 * np.pi * t)
37
+ graph.add_line_series(name="sine wave", t=t, y=y, color="blue")
38
+
39
+ # Display the visualization
40
+ graph.show(open_in_browser=True, title="Quick Start Example")
41
+ ```
42
+
43
+ ## Available Views
44
+
45
+ figpack provides a comprehensive set of view components for creating interactive visualizations:
46
+
47
+ ### Core Views
48
+
49
+ - **[TimeseriesGraph](docs/timeseries-graph.md)** - Interactive line plots, markers, and intervals ([example](examples/example_timeseries_graph.py))
50
+ - **[MultiChannelTimeseries](docs/multichannel-timeseries.md)** - Multi-channel timeseries visualization ([example](examples/example_multichannel_timeseries.py))
51
+ - **[Image](docs/image.md)** - Display images with optional annotations ([example](examples/example_image.py))
52
+ - **[Markdown](docs/markdown.md)** - Render markdown content ([example](examples/example_markdown.py))
53
+
54
+ ### Layout Views
55
+
56
+ - **[Box](docs/box.md)** - Flexible container with horizontal/vertical layouts ([example](examples/example_box.py))
57
+ - **[Splitter](docs/splitter.md)** - Resizable split panes ([example](examples/example_splitter.py))
58
+ - **[TabLayout](docs/tab-layout.md)** - Tabbed interface for multiple views ([example](examples/example_tablayout.py))
59
+
60
+ ### External Figure Support
61
+
62
+ - **[MatplotlibFigure](docs/matplotlib-figure.md)** - Embed matplotlib plots ([example](examples/example_matplotlib.py))
63
+ - **[PlotlyFigure](docs/plotly-figure.md)** - Embed plotly visualizations ([example](examples/example_plotly.py))
64
+
65
+ ### Spike Sorting Views
66
+
67
+ - **[Autocorrelograms](docs/autocorrelograms.md)** - Auto-correlation analysis ([example](examples/example_autocorrelograms.py))
68
+ - **[CrossCorrelograms](docs/cross-correlograms.md)** - Cross-correlation analysis ([example](examples/example_cross_correlograms.py))
69
+ - **[UnitsTable](docs/units-table.md)** - Sortable table for spike sorting units ([example](examples/example_units_table.py))
70
+
71
+ ## Examples
72
+
73
+ See the `examples/` directory for working examples of each view type.
74
+
75
+ ## Usage Modes
76
+
77
+ ### Local-only Mode
78
+
79
+ ```python
80
+ view.show(open_in_browser=True, title="Local Visualization")
81
+ ```
82
+
83
+ ### Sharing Online
84
+
85
+ Set the `FIGPACK_API_KEY` environment variable and use:
86
+
87
+ ```python
88
+ view.show(upload=True, open_in_browser=True, title="Shared Visualization")
89
+ ```
90
+
91
+ ### Development Mode
92
+
93
+ Set `_dev=True` in the call to show() to enable development mode, which allows for live updates and development of figpack-gui.
94
+
95
+ ## Command Line Interface
96
+
97
+ figpack includes a command-line interface for working with figures:
98
+
99
+ ### Download a Figure
100
+
101
+ ```bash
102
+ figpack download <figure-url> <dest.tar.gz>
103
+ ```
104
+
105
+ Download a figure from any figpack URL and save it as a local archive.
106
+
107
+ ### View a Figure Archive
108
+
109
+ ```bash
110
+ figpack view <figure.tar.gz>
111
+ ```
112
+
113
+ Extract and view a figure archive in your browser. The server will run locally until you press Enter.
114
+
115
+ Use `--port <number>` to specify a custom port.
116
+
117
+ ## License
118
+
119
+ Apache-2.0
120
+
121
+ ## Contributing
122
+
123
+ Visit the [GitHub repository](https://github.com/magland/figpack) for issues, contributions, and the latest updates.
@@ -0,0 +1,5 @@
1
+ """
2
+ figpack - A Python package for creating shareable, interactive visualizations in the browser
3
+ """
4
+
5
+ __version__ = "0.1.6"
@@ -3,19 +3,20 @@ Command-line interface for figpack
3
3
  """
4
4
 
5
5
  import argparse
6
- import sys
6
+ import json
7
7
  import pathlib
8
- import tempfile
8
+ import socket
9
+ import sys
9
10
  import tarfile
10
- import json
11
- import requests
11
+ import tempfile
12
12
  import threading
13
13
  import webbrowser
14
- import socket
15
14
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
- from urllib.parse import urlparse, urljoin
17
- from typing import Dict, List, Tuple, Optional, Union
18
15
  from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
16
+ from typing import Dict, List, Optional, Tuple, Union
17
+ from urllib.parse import urljoin, urlparse
18
+
19
+ import requests
19
20
 
20
21
  from . import __version__
21
22
  from .core._show_view import serve_files
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import pathlib
3
+
3
4
  import zarr
5
+
4
6
  from .figpack_view import FigpackView
5
7
 
6
8
  thisdir = pathlib.Path(__file__).parent.resolve()
@@ -1,18 +1,14 @@
1
1
  import os
2
-
3
- from typing import Union
4
- import tempfile
5
-
6
- import webbrowser
7
-
8
2
  import pathlib
9
-
3
+ import tempfile
10
4
  import threading
5
+ import webbrowser
11
6
  from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
7
+ from typing import Union
12
8
 
13
- from .figpack_view import FigpackView
14
9
  from ._bundle_utils import prepare_figure_bundle
15
10
  from ._upload_bundle import _upload_bundle
11
+ from .figpack_view import FigpackView
16
12
 
17
13
  thisdir = pathlib.Path(__file__).parent.resolve()
18
14
 
@@ -32,15 +28,15 @@ def _show_view(
32
28
 
33
29
  if upload:
34
30
  # Check for required environment variable
35
- passcode = os.environ.get("FIGPACK_UPLOAD_PASSCODE")
36
- if not passcode:
31
+ api_key = os.environ.get("FIGPACK_API_KEY")
32
+ if not api_key:
37
33
  raise EnvironmentError(
38
- "FIGPACK_UPLOAD_PASSCODE environment variable must be set to upload views."
34
+ "FIGPACK_API_KEY environment variable must be set to upload views."
39
35
  )
40
36
 
41
37
  # Upload the bundle
42
38
  print("Starting upload...")
43
- figure_url = _upload_bundle(tmpdir, passcode)
39
+ figure_url = _upload_bundle(tmpdir, api_key)
44
40
 
45
41
  if open_in_browser:
46
42
  webbrowser.open(figure_url)
@@ -0,0 +1,343 @@
1
+ import hashlib
2
+ import json
3
+ import pathlib
4
+ import threading
5
+ import time
6
+ import uuid
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from datetime import datetime, timedelta, timezone
9
+
10
+ import requests
11
+
12
+ from .. import __version__
13
+
14
+ from .config import FIGPACK_API_BASE_URL
15
+
16
+ thisdir = pathlib.Path(__file__).parent.resolve()
17
+
18
+
19
+ def _upload_single_file(
20
+ figure_url: str, relative_path: str, file_path: pathlib.Path, api_key: str
21
+ ) -> str:
22
+ """
23
+ Worker function to upload a single file using signed URL
24
+
25
+ Returns:
26
+ str: The relative path of the uploaded file
27
+ """
28
+ file_size = file_path.stat().st_size
29
+
30
+ # Get signed URL
31
+ payload = {
32
+ "figureUrl": figure_url,
33
+ "relativePath": relative_path,
34
+ "apiKey": api_key,
35
+ "size": file_size,
36
+ }
37
+
38
+ response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
39
+
40
+ if not response.ok:
41
+ try:
42
+ error_data = response.json()
43
+ error_msg = error_data.get("message", "Unknown error")
44
+ except:
45
+ error_msg = f"HTTP {response.status_code}"
46
+ raise Exception(f"Failed to get signed URL for {relative_path}: {error_msg}")
47
+
48
+ response_data = response.json()
49
+ if not response_data.get("success"):
50
+ raise Exception(
51
+ f"Failed to get signed URL for {relative_path}: {response_data.get('message', 'Unknown error')}"
52
+ )
53
+
54
+ signed_url = response_data.get("signedUrl")
55
+ if not signed_url:
56
+ raise Exception(f"No signed URL returned for {relative_path}")
57
+
58
+ # Upload file to signed URL
59
+ content_type = _determine_content_type(relative_path)
60
+ with open(file_path, "rb") as f:
61
+ upload_response = requests.put(
62
+ signed_url, data=f, headers={"Content-Type": content_type}
63
+ )
64
+
65
+ if not upload_response.ok:
66
+ raise Exception(
67
+ f"Failed to upload {relative_path} to signed URL: HTTP {upload_response.status_code}"
68
+ )
69
+
70
+ return relative_path
71
+
72
+
73
+ MAX_WORKERS_FOR_UPLOAD = 16
74
+
75
+
76
+ def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
77
+ """
78
+ Compute a deterministic figure ID based on SHA1 hashes of all files
79
+
80
+ Returns:
81
+ str: 40-character SHA1 hash representing the content of all files
82
+ """
83
+ file_hashes = []
84
+
85
+ # Collect all files and their hashes
86
+ for file_path in sorted(tmpdir_path.rglob("*")):
87
+ if file_path.is_file():
88
+ relative_path = file_path.relative_to(tmpdir_path)
89
+
90
+ # Compute SHA1 hash of file content
91
+ sha1_hash = hashlib.sha1()
92
+ with open(file_path, "rb") as f:
93
+ for chunk in iter(lambda: f.read(4096), b""):
94
+ sha1_hash.update(chunk)
95
+
96
+ # Include both the relative path and content hash to ensure uniqueness
97
+ file_info = f"{relative_path}:{sha1_hash.hexdigest()}"
98
+ file_hashes.append(file_info)
99
+
100
+ # Create final hash from all file hashes
101
+ combined_hash = hashlib.sha1()
102
+ for file_hash in file_hashes:
103
+ combined_hash.update(file_hash.encode("utf-8"))
104
+
105
+ return combined_hash.hexdigest()
106
+
107
+
108
+ def _create_or_get_figure(
109
+ figure_hash: str, api_key: str, total_files: int = None, total_size: int = None
110
+ ) -> dict:
111
+ """
112
+ Create a new figure or get existing figure information
113
+
114
+ Returns:
115
+ dict: Figure information from the API
116
+ """
117
+ payload = {
118
+ "figureHash": figure_hash,
119
+ "apiKey": api_key,
120
+ "figpackVersion": __version__,
121
+ }
122
+
123
+ if total_files is not None:
124
+ payload["totalFiles"] = total_files
125
+ if total_size is not None:
126
+ payload["totalSize"] = total_size
127
+
128
+ response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
129
+
130
+ if not response.ok:
131
+ try:
132
+ error_data = response.json()
133
+ error_msg = error_data.get("message", "Unknown error")
134
+ except:
135
+ error_msg = f"HTTP {response.status_code}"
136
+ raise Exception(f"Failed to create figure {figure_hash}: {error_msg}")
137
+
138
+ response_data = response.json()
139
+ if not response_data.get("success"):
140
+ raise Exception(
141
+ f"Failed to create figure {figure_hash}: {response_data.get('message', 'Unknown error')}"
142
+ )
143
+
144
+ return response_data
145
+
146
+
147
+ def _finalize_figure(figure_url: str, api_key: str) -> dict:
148
+ """
149
+ Finalize a figure upload
150
+
151
+ Returns:
152
+ dict: Figure information from the API
153
+ """
154
+ payload = {
155
+ "figureUrl": figure_url,
156
+ "apiKey": api_key,
157
+ }
158
+
159
+ response = requests.post(
160
+ f"{FIGPACK_API_BASE_URL}/api/figures/finalize", json=payload
161
+ )
162
+
163
+ if not response.ok:
164
+ try:
165
+ error_data = response.json()
166
+ error_msg = error_data.get("message", "Unknown error")
167
+ except:
168
+ error_msg = f"HTTP {response.status_code}"
169
+ raise Exception(f"Failed to finalize figure {figure_url}: {error_msg}")
170
+
171
+ response_data = response.json()
172
+ if not response_data.get("success"):
173
+ raise Exception(
174
+ f"Failed to finalize figure {figure_url}: {response_data.get('message', 'Unknown error')}"
175
+ )
176
+
177
+ return response_data
178
+
179
+
180
+ def _upload_bundle(tmpdir: str, api_key: str) -> str:
181
+ """
182
+ Upload the prepared bundle to the cloud using the new database-driven approach
183
+ """
184
+ tmpdir_path = pathlib.Path(tmpdir)
185
+
186
+ # Compute deterministic figure ID based on file contents
187
+ print("Computing deterministic figure ID...")
188
+ figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
189
+ print(f"Figure hash: {figure_hash}")
190
+
191
+ # Collect all files to upload
192
+ all_files = []
193
+ for file_path in tmpdir_path.rglob("*"):
194
+ if file_path.is_file():
195
+ relative_path = file_path.relative_to(tmpdir_path)
196
+ all_files.append((str(relative_path), file_path))
197
+
198
+ # Calculate total files and size for metadata
199
+ total_files = len(all_files)
200
+ total_size = sum(file_path.stat().st_size for _, file_path in all_files)
201
+ print(
202
+ f"Found {total_files} files to upload, total size: {total_size / (1024 * 1024):.2f} MB"
203
+ )
204
+
205
+ # Find available figure ID and create/get figure in database with metadata
206
+ result = _create_or_get_figure(figure_hash, api_key, total_files, total_size)
207
+ figure_info = result.get("figure", {})
208
+ figure_url = figure_info.get("figureUrl")
209
+
210
+ if figure_info["status"] == "completed":
211
+ print(f"Figure already exists at: {figure_url}")
212
+ return figure_url
213
+
214
+ print(f"Using figure URL: {figure_url}")
215
+
216
+ files_to_upload = all_files
217
+ total_files_to_upload = len(files_to_upload)
218
+
219
+ if total_files_to_upload == 0:
220
+ print("No files to upload")
221
+ else:
222
+ print(
223
+ f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
224
+ )
225
+
226
+ # Thread-safe progress tracking
227
+ uploaded_count = 0
228
+ count_lock = threading.Lock()
229
+
230
+ # Upload files in parallel with concurrent uploads
231
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
232
+ # Submit all upload tasks
233
+ future_to_file = {
234
+ executor.submit(
235
+ _upload_single_file, figure_url, rel_path, file_path, api_key
236
+ ): rel_path
237
+ for rel_path, file_path in files_to_upload
238
+ }
239
+
240
+ # Process completed uploads
241
+ for future in as_completed(future_to_file):
242
+ relative_path = future_to_file[future]
243
+ try:
244
+ future.result() # This will raise any exception that occurred during upload
245
+
246
+ # Thread-safe progress update
247
+ with count_lock:
248
+ uploaded_count += 1
249
+ print(
250
+ f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
251
+ )
252
+
253
+ except Exception as e:
254
+ print(f"Failed to upload {relative_path}: {e}")
255
+ raise # Re-raise the exception to stop the upload process
256
+
257
+ # Create manifest for finalization
258
+ print("Creating manifest...")
259
+ manifest = {
260
+ "timestamp": time.time(),
261
+ "files": [],
262
+ "total_size": 0,
263
+ "total_files": len(files_to_upload),
264
+ }
265
+
266
+ for rel_path, file_path in files_to_upload:
267
+ file_size = file_path.stat().st_size
268
+ manifest["files"].append({"path": rel_path, "size": file_size})
269
+ manifest["total_size"] += file_size
270
+
271
+ print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
272
+
273
+ # Upload manifest.json
274
+ print("Uploading manifest.json...")
275
+ manifest_content = json.dumps(manifest, indent=2)
276
+ manifest_size = len(manifest_content.encode("utf-8"))
277
+
278
+ manifest_payload = {
279
+ "figureUrl": figure_url,
280
+ "relativePath": "manifest.json",
281
+ "apiKey": api_key,
282
+ "size": manifest_size,
283
+ }
284
+
285
+ response = requests.post(
286
+ f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
287
+ )
288
+ if not response.ok:
289
+ try:
290
+ error_data = response.json()
291
+ error_msg = error_data.get("message", "Unknown error")
292
+ except:
293
+ error_msg = f"HTTP {response.status_code}"
294
+ raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
295
+
296
+ response_data = response.json()
297
+ if not response_data.get("success"):
298
+ raise Exception(
299
+ f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
300
+ )
301
+
302
+ signed_url = response_data.get("signedUrl")
303
+ if not signed_url:
304
+ raise Exception("No signed URL returned for manifest.json")
305
+
306
+ # Upload manifest using signed URL
307
+ upload_response = requests.put(
308
+ signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
309
+ )
310
+
311
+ if not upload_response.ok:
312
+ raise Exception(
313
+ f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
314
+ )
315
+
316
+ # Finalize the figure upload
317
+ print("Finalizing figure...")
318
+ _finalize_figure(figure_url, api_key)
319
+ print("Upload completed successfully")
320
+
321
+ return figure_url
322
+
323
+
324
+ def _determine_content_type(file_path: str) -> str:
325
+ """
326
+ Determine content type for upload based on file extension
327
+ """
328
+ file_name = file_path.split("/")[-1]
329
+ extension = file_name.split(".")[-1] if "." in file_name else ""
330
+
331
+ content_type_map = {
332
+ "json": "application/json",
333
+ "html": "text/html",
334
+ "css": "text/css",
335
+ "js": "application/javascript",
336
+ "png": "image/png",
337
+ "zattrs": "application/json",
338
+ "zgroup": "application/json",
339
+ "zarray": "application/json",
340
+ "zmetadata": "application/json",
341
+ }
342
+
343
+ return content_type_map.get(extension, "application/octet-stream")
@@ -0,0 +1 @@
1
+ FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
@@ -49,6 +49,7 @@ class FigpackView:
49
49
  print(
50
50
  f"For development, run figpack-gui in dev mode and use http://localhost:5173?data=http://localhost:{port}/data.zarr"
51
51
  )
52
+ open_in_browser = False
52
53
 
53
54
  _show_view(
54
55
  self,