figpack 0.1.5__tar.gz → 0.1.7__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.1.5/figpack.egg-info → figpack-0.1.7}/PKG-INFO +2 -2
- {figpack-0.1.5 → figpack-0.1.7}/README.md +1 -1
- {figpack-0.1.5 → figpack-0.1.7}/figpack/__init__.py +1 -1
- {figpack-0.1.5 → figpack-0.1.7}/figpack/core/_show_view.py +4 -4
- figpack-0.1.7/figpack/core/_upload_bundle.py +358 -0
- figpack-0.1.7/figpack/core/config.py +5 -0
- figpack-0.1.5/figpack/figpack-gui-dist/assets/index-DeyVLaXh.js → figpack-0.1.7/figpack/figpack-gui-dist/assets/index-DaeClgi6.js +1 -1
- {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/index.html +1 -1
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Box.py +6 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Image.py +8 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/MultiChannelTimeseries.py +6 -2
- {figpack-0.1.5 → figpack-0.1.7/figpack.egg-info}/PKG-INFO +2 -2
- {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/SOURCES.txt +11 -6
- {figpack-0.1.5 → figpack-0.1.7}/pyproject.toml +1 -1
- figpack-0.1.7/tests/test_box.py +62 -0
- figpack-0.1.7/tests/test_cli.py +275 -0
- figpack-0.1.7/tests/test_core.py +62 -0
- figpack-0.1.7/tests/test_figpack_view.py +106 -0
- figpack-0.1.7/tests/test_image.py +60 -0
- figpack-0.1.7/tests/test_markdown.py +65 -0
- figpack-0.1.7/tests/test_matplotlib_figure.py +111 -0
- figpack-0.1.7/tests/test_multichannel_timeseries.py +221 -0
- figpack-0.1.7/tests/test_plotly_figure.py +124 -0
- figpack-0.1.7/tests/test_show_view.py +521 -0
- figpack-0.1.7/tests/test_spike_sorting_correlograms.py +133 -0
- figpack-0.1.7/tests/test_splitter.py +108 -0
- figpack-0.1.7/tests/test_tablayout.py +150 -0
- figpack-0.1.7/tests/test_timeseries_graph.py +275 -0
- figpack-0.1.7/tests/test_units_table.py +172 -0
- figpack-0.1.7/tests/test_upload_bundle.py +679 -0
- figpack-0.1.5/figpack/core/_upload_bundle.py +0 -453
- figpack-0.1.5/tests/test_cli.py +0 -306
- figpack-0.1.5/tests/test_matplotlib_figure.py +0 -181
- figpack-0.1.5/tests/test_multichannel_timeseries.py +0 -118
- figpack-0.1.5/tests/test_package.py +0 -51
- figpack-0.1.5/tests/test_plotly_figure.py +0 -304
- figpack-0.1.5/tests/test_show_view.py +0 -433
- figpack-0.1.5/tests/test_si_autocorrelograms.py +0 -60
- figpack-0.1.5/tests/test_si_cross_correlograms.py +0 -44
- figpack-0.1.5/tests/test_splitter.py +0 -362
- figpack-0.1.5/tests/test_timeseries_graph.py +0 -583
- figpack-0.1.5/tests/test_upload_bundle.py +0 -629
- figpack-0.1.5/tests/test_views.py +0 -244
- {figpack-0.1.5 → figpack-0.1.7}/LICENSE +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/MANIFEST.in +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/cli.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/core/__init__.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/core/_bundle_utils.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/core/figpack_view.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/__init__.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTable.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/spike_sorting/views/__init__.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/LayoutItem.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Markdown.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/MatplotlibFigure.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/PlotlyFigure.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/Splitter.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TabLayout.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TabLayoutItem.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/TimeseriesGraph.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack/views/__init__.py +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/requires.txt +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.1.5 → figpack-0.1.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: figpack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -126,7 +126,7 @@ view.show(open_in_browser=True, title="Local Visualization")
|
|
|
126
126
|
|
|
127
127
|
### Sharing Online
|
|
128
128
|
|
|
129
|
-
Set the `
|
|
129
|
+
Set the `FIGPACK_API_KEY` environment variable and use:
|
|
130
130
|
|
|
131
131
|
```python
|
|
132
132
|
view.show(upload=True, open_in_browser=True, title="Shared Visualization")
|
|
@@ -82,7 +82,7 @@ view.show(open_in_browser=True, title="Local Visualization")
|
|
|
82
82
|
|
|
83
83
|
### Sharing Online
|
|
84
84
|
|
|
85
|
-
Set the `
|
|
85
|
+
Set the `FIGPACK_API_KEY` environment variable and use:
|
|
86
86
|
|
|
87
87
|
```python
|
|
88
88
|
view.show(upload=True, open_in_browser=True, title="Shared Visualization")
|
|
@@ -28,15 +28,15 @@ def _show_view(
|
|
|
28
28
|
|
|
29
29
|
if upload:
|
|
30
30
|
# Check for required environment variable
|
|
31
|
-
|
|
32
|
-
if not
|
|
31
|
+
api_key = os.environ.get("FIGPACK_API_KEY")
|
|
32
|
+
if not api_key:
|
|
33
33
|
raise EnvironmentError(
|
|
34
|
-
"
|
|
34
|
+
"FIGPACK_API_KEY environment variable must be set to upload views."
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
# Upload the bundle
|
|
38
38
|
print("Starting upload...")
|
|
39
|
-
figure_url = _upload_bundle(tmpdir,
|
|
39
|
+
figure_url = _upload_bundle(tmpdir, api_key, title=title)
|
|
40
40
|
|
|
41
41
|
if open_in_browser:
|
|
42
42
|
webbrowser.open(figure_url)
|
|
@@ -0,0 +1,358 @@
|
|
|
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,
|
|
110
|
+
api_key: str,
|
|
111
|
+
total_files: int = None,
|
|
112
|
+
total_size: int = None,
|
|
113
|
+
title: str = None,
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""
|
|
116
|
+
Create a new figure or get existing figure information
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
figure_hash: The hash of the figure
|
|
120
|
+
api_key: The API key for authentication
|
|
121
|
+
total_files: Optional total number of files
|
|
122
|
+
total_size: Optional total size of files
|
|
123
|
+
title: Optional title for the figure
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict: Figure information from the API
|
|
127
|
+
"""
|
|
128
|
+
payload = {
|
|
129
|
+
"figureHash": figure_hash,
|
|
130
|
+
"apiKey": api_key,
|
|
131
|
+
"figpackVersion": __version__,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if total_files is not None:
|
|
135
|
+
payload["totalFiles"] = total_files
|
|
136
|
+
if total_size is not None:
|
|
137
|
+
payload["totalSize"] = total_size
|
|
138
|
+
if title is not None:
|
|
139
|
+
payload["title"] = title
|
|
140
|
+
|
|
141
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
|
|
142
|
+
|
|
143
|
+
if not response.ok:
|
|
144
|
+
try:
|
|
145
|
+
error_data = response.json()
|
|
146
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
147
|
+
except:
|
|
148
|
+
error_msg = f"HTTP {response.status_code}"
|
|
149
|
+
raise Exception(f"Failed to create figure {figure_hash}: {error_msg}")
|
|
150
|
+
|
|
151
|
+
response_data = response.json()
|
|
152
|
+
if not response_data.get("success"):
|
|
153
|
+
raise Exception(
|
|
154
|
+
f"Failed to create figure {figure_hash}: {response_data.get('message', 'Unknown error')}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return response_data
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _finalize_figure(figure_url: str, api_key: str) -> dict:
|
|
161
|
+
"""
|
|
162
|
+
Finalize a figure upload
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
dict: Figure information from the API
|
|
166
|
+
"""
|
|
167
|
+
payload = {
|
|
168
|
+
"figureUrl": figure_url,
|
|
169
|
+
"apiKey": api_key,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
response = requests.post(
|
|
173
|
+
f"{FIGPACK_API_BASE_URL}/api/figures/finalize", json=payload
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if not response.ok:
|
|
177
|
+
try:
|
|
178
|
+
error_data = response.json()
|
|
179
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
180
|
+
except:
|
|
181
|
+
error_msg = f"HTTP {response.status_code}"
|
|
182
|
+
raise Exception(f"Failed to finalize figure {figure_url}: {error_msg}")
|
|
183
|
+
|
|
184
|
+
response_data = response.json()
|
|
185
|
+
if not response_data.get("success"):
|
|
186
|
+
raise Exception(
|
|
187
|
+
f"Failed to finalize figure {figure_url}: {response_data.get('message', 'Unknown error')}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return response_data
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _upload_bundle(tmpdir: str, api_key: str, title: str = None) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Upload the prepared bundle to the cloud using the new database-driven approach
|
|
196
|
+
"""
|
|
197
|
+
tmpdir_path = pathlib.Path(tmpdir)
|
|
198
|
+
|
|
199
|
+
# Compute deterministic figure ID based on file contents
|
|
200
|
+
print("Computing deterministic figure ID...")
|
|
201
|
+
figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
|
|
202
|
+
print(f"Figure hash: {figure_hash}")
|
|
203
|
+
|
|
204
|
+
# Collect all files to upload
|
|
205
|
+
all_files = []
|
|
206
|
+
for file_path in tmpdir_path.rglob("*"):
|
|
207
|
+
if file_path.is_file():
|
|
208
|
+
relative_path = file_path.relative_to(tmpdir_path)
|
|
209
|
+
all_files.append((str(relative_path), file_path))
|
|
210
|
+
|
|
211
|
+
# Calculate total files and size for metadata
|
|
212
|
+
total_files = len(all_files)
|
|
213
|
+
total_size = sum(file_path.stat().st_size for _, file_path in all_files)
|
|
214
|
+
print(
|
|
215
|
+
f"Found {total_files} files to upload, total size: {total_size / (1024 * 1024):.2f} MB"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Find available figure ID and create/get figure in database with metadata
|
|
219
|
+
result = _create_or_get_figure(
|
|
220
|
+
figure_hash, api_key, total_files, total_size, title=title
|
|
221
|
+
)
|
|
222
|
+
figure_info = result.get("figure", {})
|
|
223
|
+
figure_url = figure_info.get("figureUrl")
|
|
224
|
+
|
|
225
|
+
if figure_info["status"] == "completed":
|
|
226
|
+
print(f"Figure already exists at: {figure_url}")
|
|
227
|
+
return figure_url
|
|
228
|
+
|
|
229
|
+
print(f"Using figure URL: {figure_url}")
|
|
230
|
+
|
|
231
|
+
files_to_upload = all_files
|
|
232
|
+
total_files_to_upload = len(files_to_upload)
|
|
233
|
+
|
|
234
|
+
if total_files_to_upload == 0:
|
|
235
|
+
print("No files to upload")
|
|
236
|
+
else:
|
|
237
|
+
print(
|
|
238
|
+
f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Thread-safe progress tracking
|
|
242
|
+
uploaded_count = 0
|
|
243
|
+
count_lock = threading.Lock()
|
|
244
|
+
|
|
245
|
+
# Upload files in parallel with concurrent uploads
|
|
246
|
+
with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
|
|
247
|
+
# Submit all upload tasks
|
|
248
|
+
future_to_file = {
|
|
249
|
+
executor.submit(
|
|
250
|
+
_upload_single_file, figure_url, rel_path, file_path, api_key
|
|
251
|
+
): rel_path
|
|
252
|
+
for rel_path, file_path in files_to_upload
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Process completed uploads
|
|
256
|
+
for future in as_completed(future_to_file):
|
|
257
|
+
relative_path = future_to_file[future]
|
|
258
|
+
try:
|
|
259
|
+
future.result() # This will raise any exception that occurred during upload
|
|
260
|
+
|
|
261
|
+
# Thread-safe progress update
|
|
262
|
+
with count_lock:
|
|
263
|
+
uploaded_count += 1
|
|
264
|
+
print(
|
|
265
|
+
f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
print(f"Failed to upload {relative_path}: {e}")
|
|
270
|
+
raise # Re-raise the exception to stop the upload process
|
|
271
|
+
|
|
272
|
+
# Create manifest for finalization
|
|
273
|
+
print("Creating manifest...")
|
|
274
|
+
manifest = {
|
|
275
|
+
"timestamp": time.time(),
|
|
276
|
+
"files": [],
|
|
277
|
+
"total_size": 0,
|
|
278
|
+
"total_files": len(files_to_upload),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for rel_path, file_path in files_to_upload:
|
|
282
|
+
file_size = file_path.stat().st_size
|
|
283
|
+
manifest["files"].append({"path": rel_path, "size": file_size})
|
|
284
|
+
manifest["total_size"] += file_size
|
|
285
|
+
|
|
286
|
+
print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
|
|
287
|
+
|
|
288
|
+
# Upload manifest.json
|
|
289
|
+
print("Uploading manifest.json...")
|
|
290
|
+
manifest_content = json.dumps(manifest, indent=2)
|
|
291
|
+
manifest_size = len(manifest_content.encode("utf-8"))
|
|
292
|
+
|
|
293
|
+
manifest_payload = {
|
|
294
|
+
"figureUrl": figure_url,
|
|
295
|
+
"relativePath": "manifest.json",
|
|
296
|
+
"apiKey": api_key,
|
|
297
|
+
"size": manifest_size,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
response = requests.post(
|
|
301
|
+
f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
|
|
302
|
+
)
|
|
303
|
+
if not response.ok:
|
|
304
|
+
try:
|
|
305
|
+
error_data = response.json()
|
|
306
|
+
error_msg = error_data.get("message", "Unknown error")
|
|
307
|
+
except:
|
|
308
|
+
error_msg = f"HTTP {response.status_code}"
|
|
309
|
+
raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
|
|
310
|
+
|
|
311
|
+
response_data = response.json()
|
|
312
|
+
if not response_data.get("success"):
|
|
313
|
+
raise Exception(
|
|
314
|
+
f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
signed_url = response_data.get("signedUrl")
|
|
318
|
+
if not signed_url:
|
|
319
|
+
raise Exception("No signed URL returned for manifest.json")
|
|
320
|
+
|
|
321
|
+
# Upload manifest using signed URL
|
|
322
|
+
upload_response = requests.put(
|
|
323
|
+
signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if not upload_response.ok:
|
|
327
|
+
raise Exception(
|
|
328
|
+
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Finalize the figure upload
|
|
332
|
+
print("Finalizing figure...")
|
|
333
|
+
_finalize_figure(figure_url, api_key)
|
|
334
|
+
print("Upload completed successfully")
|
|
335
|
+
|
|
336
|
+
return figure_url
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _determine_content_type(file_path: str) -> str:
|
|
340
|
+
"""
|
|
341
|
+
Determine content type for upload based on file extension
|
|
342
|
+
"""
|
|
343
|
+
file_name = file_path.split("/")[-1]
|
|
344
|
+
extension = file_name.split(".")[-1] if "." in file_name else ""
|
|
345
|
+
|
|
346
|
+
content_type_map = {
|
|
347
|
+
"json": "application/json",
|
|
348
|
+
"html": "text/html",
|
|
349
|
+
"css": "text/css",
|
|
350
|
+
"js": "application/javascript",
|
|
351
|
+
"png": "image/png",
|
|
352
|
+
"zattrs": "application/json",
|
|
353
|
+
"zgroup": "application/json",
|
|
354
|
+
"zarray": "application/json",
|
|
355
|
+
"zmetadata": "application/json",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return content_type_map.get(extension, "application/octet-stream")
|