figpack 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
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/__init__.py +1 -5
- figpack/cli.py +8 -7
- figpack/core/_bundle_utils.py +2 -0
- figpack/core/_show_view.py +8 -12
- figpack/core/_upload_bundle.py +167 -275
- figpack/core/config.py +1 -0
- figpack/core/figpack_view.py +1 -0
- figpack/figpack-gui-dist/assets/{index-Dw14QqeQ.js → index-DaeClgi6.js} +91 -91
- figpack/figpack-gui-dist/index.html +1 -1
- figpack/spike_sorting/views/AutocorrelogramItem.py +1 -0
- figpack/spike_sorting/views/Autocorrelograms.py +39 -9
- figpack/spike_sorting/views/CrossCorrelogramItem.py +1 -0
- figpack/spike_sorting/views/CrossCorrelograms.py +45 -7
- figpack/spike_sorting/views/UnitsTable.py +5 -3
- figpack/spike_sorting/views/UnitsTableRow.py +1 -1
- figpack/spike_sorting/views/__init__.py +2 -2
- figpack/views/Box.py +9 -1
- figpack/views/Image.py +12 -2
- figpack/views/LayoutItem.py +1 -0
- figpack/views/Markdown.py +1 -0
- figpack/views/MatplotlibFigure.py +4 -2
- figpack/views/MultiChannelTimeseries.py +230 -0
- figpack/views/PlotlyFigure.py +5 -3
- figpack/views/Splitter.py +3 -1
- figpack/views/TabLayout.py +3 -1
- figpack/views/TabLayoutItem.py +1 -0
- figpack/views/TimeseriesGraph.py +3 -2
- figpack/views/__init__.py +7 -6
- {figpack-0.1.4.dist-info → figpack-0.1.6.dist-info}/METADATA +49 -8
- figpack-0.1.6.dist-info/RECORD +40 -0
- figpack-0.1.4.dist-info/RECORD +0 -38
- {figpack-0.1.4.dist-info → figpack-0.1.6.dist-info}/WHEEL +0 -0
- {figpack-0.1.4.dist-info → figpack-0.1.6.dist-info}/entry_points.txt +0 -0
- {figpack-0.1.4.dist-info → figpack-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {figpack-0.1.4.dist-info → figpack-0.1.6.dist-info}/top_level.txt +0 -0
figpack/__init__.py
CHANGED
figpack/cli.py
CHANGED
|
@@ -3,19 +3,20 @@ Command-line interface for figpack
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import
|
|
6
|
+
import json
|
|
7
7
|
import pathlib
|
|
8
|
-
import
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
9
10
|
import tarfile
|
|
10
|
-
import
|
|
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
|
figpack/core/_bundle_utils.py
CHANGED
figpack/core/_show_view.py
CHANGED
|
@@ -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
|
-
|
|
36
|
-
if not
|
|
31
|
+
api_key = os.environ.get("FIGPACK_API_KEY")
|
|
32
|
+
if not api_key:
|
|
37
33
|
raise EnvironmentError(
|
|
38
|
-
"
|
|
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,
|
|
39
|
+
figure_url = _upload_bundle(tmpdir, api_key)
|
|
44
40
|
|
|
45
41
|
if open_in_browser:
|
|
46
42
|
webbrowser.open(figure_url)
|
figpack/core/_upload_bundle.py
CHANGED
|
@@ -1,37 +1,71 @@
|
|
|
1
|
-
import
|
|
1
|
+
import hashlib
|
|
2
2
|
import json
|
|
3
|
-
import uuid
|
|
4
3
|
import pathlib
|
|
5
|
-
import requests
|
|
6
4
|
import threading
|
|
7
|
-
import
|
|
8
|
-
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
9
7
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
8
|
from datetime import datetime, timedelta, timezone
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .. import __version__
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
from .config import FIGPACK_API_BASE_URL
|
|
15
|
+
|
|
16
|
+
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _upload_single_file(
|
|
19
|
-
|
|
20
|
+
figure_url: str, relative_path: str, file_path: pathlib.Path, api_key: str
|
|
20
21
|
) -> str:
|
|
21
22
|
"""
|
|
22
|
-
Worker function to upload a single file
|
|
23
|
+
Worker function to upload a single file using signed URL
|
|
23
24
|
|
|
24
25
|
Returns:
|
|
25
26
|
str: The relative path of the uploaded file
|
|
26
27
|
"""
|
|
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}")
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
)
|
|
35
69
|
|
|
36
70
|
return relative_path
|
|
37
71
|
|
|
@@ -39,7 +73,7 @@ def _upload_single_file(
|
|
|
39
73
|
MAX_WORKERS_FOR_UPLOAD = 16
|
|
40
74
|
|
|
41
75
|
|
|
42
|
-
def
|
|
76
|
+
def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
|
|
43
77
|
"""
|
|
44
78
|
Compute a deterministic figure ID based on SHA1 hashes of all files
|
|
45
79
|
|
|
@@ -71,101 +105,88 @@ def _compute_deterministic_figure_id(tmpdir_path: pathlib.Path) -> str:
|
|
|
71
105
|
return combined_hash.hexdigest()
|
|
72
106
|
|
|
73
107
|
|
|
74
|
-
def
|
|
108
|
+
def _create_or_get_figure(
|
|
109
|
+
figure_hash: str, api_key: str, total_files: int = None, total_size: int = None
|
|
110
|
+
) -> dict:
|
|
75
111
|
"""
|
|
76
|
-
|
|
112
|
+
Create a new figure or get existing figure information
|
|
77
113
|
|
|
78
114
|
Returns:
|
|
79
|
-
dict:
|
|
115
|
+
dict: Figure information from the API
|
|
80
116
|
"""
|
|
81
|
-
|
|
117
|
+
payload = {
|
|
118
|
+
"figureHash": figure_hash,
|
|
119
|
+
"apiKey": api_key,
|
|
120
|
+
"figpackVersion": __version__,
|
|
121
|
+
}
|
|
82
122
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return {"exists": True, "status": figpack_data.get("status", "unknown")}
|
|
88
|
-
else:
|
|
89
|
-
return {"exists": False}
|
|
90
|
-
except Exception:
|
|
91
|
-
return {"exists": False}
|
|
123
|
+
if total_files is not None:
|
|
124
|
+
payload["totalFiles"] = total_files
|
|
125
|
+
if total_size is not None:
|
|
126
|
+
payload["totalSize"] = total_size
|
|
92
127
|
|
|
128
|
+
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
|
|
93
129
|
|
|
94
|
-
|
|
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:
|
|
95
148
|
"""
|
|
96
|
-
|
|
149
|
+
Finalize a figure upload
|
|
97
150
|
|
|
98
151
|
Returns:
|
|
99
|
-
|
|
100
|
-
- figure_id_to_use is None if upload should be skipped
|
|
101
|
-
- completed_figure_id is the ID of the completed figure if one exists
|
|
152
|
+
dict: Figure information from the API
|
|
102
153
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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')}"
|
|
110
175
|
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
candidate_id = f"{base_figure_id}-{suffix}"
|
|
117
|
-
result = _check_existing_figure(candidate_id)
|
|
118
|
-
|
|
119
|
-
if not result["exists"]:
|
|
120
|
-
print(f"Using figure ID: {candidate_id}")
|
|
121
|
-
return (candidate_id, None)
|
|
122
|
-
elif result["status"] == "completed":
|
|
123
|
-
print(
|
|
124
|
-
f"Figure {candidate_id} already exists and is completed. Skipping upload."
|
|
125
|
-
)
|
|
126
|
-
return (None, candidate_id) # Signal to skip upload, return completed ID
|
|
127
|
-
|
|
128
|
-
suffix += 1
|
|
129
|
-
if suffix > 100: # Safety limit
|
|
130
|
-
raise Exception(
|
|
131
|
-
"Too many existing figure variants, unable to find available ID"
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
176
|
+
|
|
177
|
+
return response_data
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
136
181
|
"""
|
|
137
|
-
Upload the prepared bundle to the cloud using
|
|
182
|
+
Upload the prepared bundle to the cloud using the new database-driven approach
|
|
138
183
|
"""
|
|
139
184
|
tmpdir_path = pathlib.Path(tmpdir)
|
|
140
185
|
|
|
141
186
|
# Compute deterministic figure ID based on file contents
|
|
142
187
|
print("Computing deterministic figure ID...")
|
|
143
|
-
|
|
144
|
-
print(f"
|
|
145
|
-
|
|
146
|
-
# Find available figure ID (check for existing uploads)
|
|
147
|
-
figure_id, completed_figure_id = _find_available_figure_id(base_figure_id)
|
|
148
|
-
|
|
149
|
-
# If figure_id is None, it means we found a completed upload and should skip
|
|
150
|
-
if figure_id is None:
|
|
151
|
-
figure_url = f"{TEMPORY_BASE_URL}/{completed_figure_id}/index.html"
|
|
152
|
-
print(f"Figure already exists at: {figure_url}")
|
|
153
|
-
return figure_url
|
|
154
|
-
|
|
155
|
-
print(f"Using figure ID: {figure_id}")
|
|
156
|
-
|
|
157
|
-
# First, upload initial figpack.json with "uploading" status
|
|
158
|
-
print("Uploading initial status...")
|
|
159
|
-
figpack_json = {
|
|
160
|
-
"status": "uploading",
|
|
161
|
-
"upload_started": datetime.now(timezone.utc).isoformat(),
|
|
162
|
-
"upload_updated": datetime.now(timezone.utc).isoformat(),
|
|
163
|
-
"figure_id": figure_id,
|
|
164
|
-
"figpack_version": __version__,
|
|
165
|
-
}
|
|
166
|
-
_upload_small_file(
|
|
167
|
-
figure_id, "figpack.json", json.dumps(figpack_json, indent=2), passcode
|
|
168
|
-
)
|
|
188
|
+
figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
|
|
189
|
+
print(f"Figure hash: {figure_hash}")
|
|
169
190
|
|
|
170
191
|
# Collect all files to upload
|
|
171
192
|
all_files = []
|
|
@@ -174,34 +195,44 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
174
195
|
relative_path = file_path.relative_to(tmpdir_path)
|
|
175
196
|
all_files.append((str(relative_path), file_path))
|
|
176
197
|
|
|
177
|
-
|
|
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")
|
|
178
209
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
185
217
|
total_files_to_upload = len(files_to_upload)
|
|
186
218
|
|
|
187
219
|
if total_files_to_upload == 0:
|
|
188
|
-
print("No
|
|
220
|
+
print("No files to upload")
|
|
189
221
|
else:
|
|
190
222
|
print(
|
|
191
|
-
f"Uploading {total_files_to_upload} files with up to
|
|
223
|
+
f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
|
|
192
224
|
)
|
|
193
225
|
|
|
194
226
|
# Thread-safe progress tracking
|
|
195
227
|
uploaded_count = 0
|
|
196
228
|
count_lock = threading.Lock()
|
|
197
|
-
timer = time.time()
|
|
198
229
|
|
|
199
230
|
# Upload files in parallel with concurrent uploads
|
|
200
231
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
|
|
201
232
|
# Submit all upload tasks
|
|
202
233
|
future_to_file = {
|
|
203
234
|
executor.submit(
|
|
204
|
-
_upload_single_file,
|
|
235
|
+
_upload_single_file, figure_url, rel_path, file_path, api_key
|
|
205
236
|
): rel_path
|
|
206
237
|
for rel_path, file_path in files_to_upload
|
|
207
238
|
}
|
|
@@ -219,36 +250,14 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
219
250
|
f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
|
|
220
251
|
)
|
|
221
252
|
|
|
222
|
-
# Update progress every 60 seconds
|
|
223
|
-
elapsed_time = time.time() - timer
|
|
224
|
-
if elapsed_time > 60:
|
|
225
|
-
figpack_json = {
|
|
226
|
-
**figpack_json,
|
|
227
|
-
"status": "uploading",
|
|
228
|
-
"upload_progress": f"{uploaded_count}/{total_files_to_upload}",
|
|
229
|
-
"upload_updated": datetime.now(
|
|
230
|
-
timezone.utc
|
|
231
|
-
).isoformat(),
|
|
232
|
-
}
|
|
233
|
-
_upload_small_file(
|
|
234
|
-
figure_id,
|
|
235
|
-
"figpack.json",
|
|
236
|
-
json.dumps(figpack_json, indent=2),
|
|
237
|
-
passcode,
|
|
238
|
-
)
|
|
239
|
-
print(
|
|
240
|
-
f"Updated figpack.json with progress: {uploaded_count}/{total_files_to_upload}"
|
|
241
|
-
)
|
|
242
|
-
timer = time.time()
|
|
243
|
-
|
|
244
253
|
except Exception as e:
|
|
245
254
|
print(f"Failed to upload {relative_path}: {e}")
|
|
246
255
|
raise # Re-raise the exception to stop the upload process
|
|
247
256
|
|
|
248
|
-
# Create
|
|
249
|
-
print("Creating manifest
|
|
257
|
+
# Create manifest for finalization
|
|
258
|
+
print("Creating manifest...")
|
|
250
259
|
manifest = {
|
|
251
|
-
"timestamp":
|
|
260
|
+
"timestamp": time.time(),
|
|
252
261
|
"files": [],
|
|
253
262
|
"total_size": 0,
|
|
254
263
|
"total_files": len(files_to_upload),
|
|
@@ -259,175 +268,58 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
259
268
|
manifest["files"].append({"path": rel_path, "size": file_size})
|
|
260
269
|
manifest["total_size"] += file_size
|
|
261
270
|
|
|
262
|
-
_upload_small_file(
|
|
263
|
-
figure_id, "manifest.json", json.dumps(manifest, indent=2), passcode
|
|
264
|
-
)
|
|
265
|
-
print("Uploaded manifest.json")
|
|
266
271
|
print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
|
|
267
272
|
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"upload_completed": datetime.now(timezone.utc).isoformat(),
|
|
273
|
-
"expiration": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
|
|
274
|
-
"figure_id": figure_id,
|
|
275
|
-
"total_files": len(all_files),
|
|
276
|
-
"total_size": manifest["total_size"],
|
|
277
|
-
"figpack_version": __version__,
|
|
278
|
-
}
|
|
279
|
-
_upload_small_file(
|
|
280
|
-
figure_id, "figpack.json", json.dumps(figpack_json, indent=2), passcode
|
|
281
|
-
)
|
|
282
|
-
print("Upload completed successfully")
|
|
283
|
-
|
|
284
|
-
figure_url = f"{TEMPORY_BASE_URL}/{figure_id}/index.html"
|
|
285
|
-
return figure_url
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _determine_file_type(file_path: str) -> str:
|
|
289
|
-
"""
|
|
290
|
-
Determine if a file should be uploaded as small or large
|
|
291
|
-
Based on the validation logic in the API
|
|
292
|
-
"""
|
|
293
|
-
# Check exact matches first
|
|
294
|
-
if file_path == "figpack.json" or file_path == "index.html":
|
|
295
|
-
return "small"
|
|
296
|
-
|
|
297
|
-
# Check zarr metadata files
|
|
298
|
-
if (
|
|
299
|
-
file_path.endswith(".zattrs")
|
|
300
|
-
or file_path.endswith(".zgroup")
|
|
301
|
-
or file_path.endswith(".zarray")
|
|
302
|
-
or file_path.endswith(".zmetadata")
|
|
303
|
-
):
|
|
304
|
-
return "small"
|
|
305
|
-
|
|
306
|
-
# Check HTML files
|
|
307
|
-
if file_path.endswith(".html"):
|
|
308
|
-
return "small"
|
|
309
|
-
|
|
310
|
-
# Check data.zarr directory
|
|
311
|
-
if file_path.startswith("data.zarr/"):
|
|
312
|
-
file_name = file_path[len("data.zarr/") :]
|
|
313
|
-
# Check if it's a zarr chunk (numeric like 0.0.1)
|
|
314
|
-
if _is_zarr_chunk(file_name):
|
|
315
|
-
return "large"
|
|
316
|
-
# Check for zarr metadata files in subdirectories
|
|
317
|
-
if (
|
|
318
|
-
file_name.endswith(".zattrs")
|
|
319
|
-
or file_name.endswith(".zgroup")
|
|
320
|
-
or file_name.endswith(".zarray")
|
|
321
|
-
or file_name.endswith(".zmetadata")
|
|
322
|
-
):
|
|
323
|
-
return "small"
|
|
324
|
-
|
|
325
|
-
# Check assets directory
|
|
326
|
-
if file_path.startswith("assets/"):
|
|
327
|
-
file_name = file_path[len("assets/") :]
|
|
328
|
-
if file_name.endswith(".js") or file_name.endswith(".css"):
|
|
329
|
-
return "large"
|
|
330
|
-
|
|
331
|
-
# Default to large file
|
|
332
|
-
return "large"
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def _is_zarr_chunk(file_name: str) -> bool:
|
|
336
|
-
"""
|
|
337
|
-
Check if filename consists only of numbers and dots (zarr chunk pattern)
|
|
338
|
-
"""
|
|
339
|
-
for char in file_name:
|
|
340
|
-
if char != "." and not char.isdigit():
|
|
341
|
-
return False
|
|
342
|
-
return (
|
|
343
|
-
len(file_name) > 0
|
|
344
|
-
and not file_name.startswith(".")
|
|
345
|
-
and not file_name.endswith(".")
|
|
346
|
-
)
|
|
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"))
|
|
347
277
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
Upload a small file by sending content directly
|
|
354
|
-
"""
|
|
355
|
-
destination_url = f"{TEMPORY_BASE_URL}/{figure_id}/{file_path}"
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
content.encode("utf-8")
|
|
359
|
-
except Exception as e:
|
|
360
|
-
raise Exception(f"Content for {file_path} is not UTF-8 encodable: {e}")
|
|
361
|
-
payload = {
|
|
362
|
-
"destinationUrl": destination_url,
|
|
363
|
-
"passcode": passcode,
|
|
364
|
-
"content": content,
|
|
278
|
+
manifest_payload = {
|
|
279
|
+
"figureUrl": figure_url,
|
|
280
|
+
"relativePath": "manifest.json",
|
|
281
|
+
"apiKey": api_key,
|
|
282
|
+
"size": manifest_size,
|
|
365
283
|
}
|
|
366
|
-
# check that payload is json serializable
|
|
367
|
-
try:
|
|
368
|
-
json.dumps(payload)
|
|
369
|
-
except Exception as e:
|
|
370
|
-
raise Exception(f"Payload for {file_path} is not JSON serializable: {e}")
|
|
371
|
-
|
|
372
|
-
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
373
|
-
|
|
374
|
-
if not response.ok:
|
|
375
|
-
try:
|
|
376
|
-
error_data = response.json()
|
|
377
|
-
error_msg = error_data.get("message", "Unknown error")
|
|
378
|
-
except:
|
|
379
|
-
error_msg = f"HTTP {response.status_code}"
|
|
380
|
-
raise Exception(f"Failed to upload {file_path}: {error_msg}")
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def _upload_large_file(
|
|
384
|
-
figure_id: str, file_path: str, local_file_path: pathlib.Path, passcode: str
|
|
385
|
-
) -> None:
|
|
386
|
-
"""
|
|
387
|
-
Upload a large file using signed URL
|
|
388
|
-
"""
|
|
389
|
-
destination_url = f"{TEMPORY_BASE_URL}/{figure_id}/{file_path}"
|
|
390
|
-
file_size = local_file_path.stat().st_size
|
|
391
|
-
|
|
392
|
-
# Get signed URL
|
|
393
|
-
payload = {
|
|
394
|
-
"destinationUrl": destination_url,
|
|
395
|
-
"passcode": passcode,
|
|
396
|
-
"size": file_size,
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
400
284
|
|
|
285
|
+
response = requests.post(
|
|
286
|
+
f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
|
|
287
|
+
)
|
|
401
288
|
if not response.ok:
|
|
402
289
|
try:
|
|
403
290
|
error_data = response.json()
|
|
404
291
|
error_msg = error_data.get("message", "Unknown error")
|
|
405
292
|
except:
|
|
406
293
|
error_msg = f"HTTP {response.status_code}"
|
|
407
|
-
raise Exception(f"Failed to get signed URL for
|
|
294
|
+
raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
|
|
408
295
|
|
|
409
296
|
response_data = response.json()
|
|
410
297
|
if not response_data.get("success"):
|
|
411
298
|
raise Exception(
|
|
412
|
-
f"Failed to get signed URL for
|
|
299
|
+
f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
|
|
413
300
|
)
|
|
414
301
|
|
|
415
302
|
signed_url = response_data.get("signedUrl")
|
|
416
303
|
if not signed_url:
|
|
417
|
-
raise Exception(
|
|
304
|
+
raise Exception("No signed URL returned for manifest.json")
|
|
418
305
|
|
|
419
|
-
# Upload
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
signed_url, data=f, headers={"Content-Type": content_type}
|
|
424
|
-
)
|
|
306
|
+
# Upload manifest using signed URL
|
|
307
|
+
upload_response = requests.put(
|
|
308
|
+
signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
|
|
309
|
+
)
|
|
425
310
|
|
|
426
311
|
if not upload_response.ok:
|
|
427
312
|
raise Exception(
|
|
428
|
-
f"Failed to upload
|
|
313
|
+
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
429
314
|
)
|
|
430
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
|
+
|
|
431
323
|
|
|
432
324
|
def _determine_content_type(file_path: str) -> str:
|
|
433
325
|
"""
|
figpack/core/config.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
|