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