figpack 0.1.5__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 -1
- figpack/core/_show_view.py +4 -4
- figpack/core/_upload_bundle.py +160 -270
- figpack/core/config.py +1 -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.6.dist-info}/METADATA +2 -2
- {figpack-0.1.5.dist-info → figpack-0.1.6.dist-info}/RECORD +15 -14
- {figpack-0.1.5.dist-info → figpack-0.1.6.dist-info}/WHEEL +0 -0
- {figpack-0.1.5.dist-info → figpack-0.1.6.dist-info}/entry_points.txt +0 -0
- {figpack-0.1.5.dist-info → figpack-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {figpack-0.1.5.dist-info → figpack-0.1.6.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)
|
|
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,88 @@ 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, api_key: str, total_files: int = None, total_size: int = None
|
|
110
|
+
) -> dict:
|
|
77
111
|
"""
|
|
78
|
-
|
|
112
|
+
Create a new figure or get existing figure information
|
|
79
113
|
|
|
80
114
|
Returns:
|
|
81
|
-
dict:
|
|
115
|
+
dict: Figure information from the API
|
|
82
116
|
"""
|
|
83
|
-
|
|
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
|
+
)
|
|
84
143
|
|
|
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}
|
|
144
|
+
return response_data
|
|
94
145
|
|
|
95
146
|
|
|
96
|
-
def
|
|
147
|
+
def _finalize_figure(figure_url: str, api_key: str) -> dict:
|
|
97
148
|
"""
|
|
98
|
-
|
|
149
|
+
Finalize a figure upload
|
|
99
150
|
|
|
100
151
|
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
|
|
152
|
+
dict: Figure information from the API
|
|
104
153
|
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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')}"
|
|
112
175
|
)
|
|
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:
|
|
176
|
+
|
|
177
|
+
return response_data
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
138
181
|
"""
|
|
139
|
-
Upload the prepared bundle to the cloud using
|
|
182
|
+
Upload the prepared bundle to the cloud using the new database-driven approach
|
|
140
183
|
"""
|
|
141
184
|
tmpdir_path = pathlib.Path(tmpdir)
|
|
142
185
|
|
|
143
186
|
# Compute deterministic figure ID based on file contents
|
|
144
187
|
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
|
-
)
|
|
188
|
+
figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
|
|
189
|
+
print(f"Figure hash: {figure_hash}")
|
|
171
190
|
|
|
172
191
|
# Collect all files to upload
|
|
173
192
|
all_files = []
|
|
@@ -176,34 +195,44 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
176
195
|
relative_path = file_path.relative_to(tmpdir_path)
|
|
177
196
|
all_files.append((str(relative_path), file_path))
|
|
178
197
|
|
|
179
|
-
|
|
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
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for rel_path, file_path in all_files
|
|
185
|
-
if rel_path != "figpack.json"
|
|
186
|
-
]
|
|
214
|
+
print(f"Using figure URL: {figure_url}")
|
|
215
|
+
|
|
216
|
+
files_to_upload = all_files
|
|
187
217
|
total_files_to_upload = len(files_to_upload)
|
|
188
218
|
|
|
189
219
|
if total_files_to_upload == 0:
|
|
190
|
-
print("No
|
|
220
|
+
print("No files to upload")
|
|
191
221
|
else:
|
|
192
222
|
print(
|
|
193
|
-
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..."
|
|
194
224
|
)
|
|
195
225
|
|
|
196
226
|
# Thread-safe progress tracking
|
|
197
227
|
uploaded_count = 0
|
|
198
228
|
count_lock = threading.Lock()
|
|
199
|
-
timer = time.time()
|
|
200
229
|
|
|
201
230
|
# Upload files in parallel with concurrent uploads
|
|
202
231
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
|
|
203
232
|
# Submit all upload tasks
|
|
204
233
|
future_to_file = {
|
|
205
234
|
executor.submit(
|
|
206
|
-
_upload_single_file,
|
|
235
|
+
_upload_single_file, figure_url, rel_path, file_path, api_key
|
|
207
236
|
): rel_path
|
|
208
237
|
for rel_path, file_path in files_to_upload
|
|
209
238
|
}
|
|
@@ -221,36 +250,14 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
221
250
|
f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
|
|
222
251
|
)
|
|
223
252
|
|
|
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
253
|
except Exception as e:
|
|
247
254
|
print(f"Failed to upload {relative_path}: {e}")
|
|
248
255
|
raise # Re-raise the exception to stop the upload process
|
|
249
256
|
|
|
250
|
-
# Create
|
|
251
|
-
print("Creating manifest
|
|
257
|
+
# Create manifest for finalization
|
|
258
|
+
print("Creating manifest...")
|
|
252
259
|
manifest = {
|
|
253
|
-
"timestamp":
|
|
260
|
+
"timestamp": time.time(),
|
|
254
261
|
"files": [],
|
|
255
262
|
"total_size": 0,
|
|
256
263
|
"total_files": len(files_to_upload),
|
|
@@ -261,175 +268,58 @@ def _upload_bundle(tmpdir: str, passcode: str) -> None:
|
|
|
261
268
|
manifest["files"].append({"path": rel_path, "size": file_size})
|
|
262
269
|
manifest["total_size"] += file_size
|
|
263
270
|
|
|
264
|
-
_upload_small_file(
|
|
265
|
-
figure_id, "manifest.json", json.dumps(manifest, indent=2), passcode
|
|
266
|
-
)
|
|
267
|
-
print("Uploaded manifest.json")
|
|
268
271
|
print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
|
|
269
272
|
|
|
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
|
-
|
|
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"))
|
|
350
277
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"""
|
|
357
|
-
destination_url = f"{FIGPACK_FIGURES_BASE_URL}/{figure_id}/{file_path}"
|
|
358
|
-
|
|
359
|
-
try:
|
|
360
|
-
content.encode("utf-8")
|
|
361
|
-
except Exception as e:
|
|
362
|
-
raise Exception(f"Content for {file_path} is not UTF-8 encodable: {e}")
|
|
363
|
-
payload = {
|
|
364
|
-
"destinationUrl": destination_url,
|
|
365
|
-
"passcode": passcode,
|
|
366
|
-
"content": content,
|
|
367
|
-
}
|
|
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,
|
|
278
|
+
manifest_payload = {
|
|
279
|
+
"figureUrl": figure_url,
|
|
280
|
+
"relativePath": "manifest.json",
|
|
281
|
+
"apiKey": api_key,
|
|
282
|
+
"size": manifest_size,
|
|
399
283
|
}
|
|
400
284
|
|
|
401
|
-
response = requests.post(
|
|
402
|
-
|
|
285
|
+
response = requests.post(
|
|
286
|
+
f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
|
|
287
|
+
)
|
|
403
288
|
if not response.ok:
|
|
404
289
|
try:
|
|
405
290
|
error_data = response.json()
|
|
406
291
|
error_msg = error_data.get("message", "Unknown error")
|
|
407
292
|
except:
|
|
408
293
|
error_msg = f"HTTP {response.status_code}"
|
|
409
|
-
raise Exception(f"Failed to get signed URL for
|
|
294
|
+
raise Exception(f"Failed to get signed URL for manifest.json: {error_msg}")
|
|
410
295
|
|
|
411
296
|
response_data = response.json()
|
|
412
297
|
if not response_data.get("success"):
|
|
413
298
|
raise Exception(
|
|
414
|
-
f"Failed to get signed URL for
|
|
299
|
+
f"Failed to get signed URL for manifest.json: {response_data.get('message', 'Unknown error')}"
|
|
415
300
|
)
|
|
416
301
|
|
|
417
302
|
signed_url = response_data.get("signedUrl")
|
|
418
303
|
if not signed_url:
|
|
419
|
-
raise Exception(
|
|
304
|
+
raise Exception("No signed URL returned for manifest.json")
|
|
420
305
|
|
|
421
|
-
# Upload
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
signed_url, data=f, headers={"Content-Type": content_type}
|
|
426
|
-
)
|
|
306
|
+
# Upload manifest using signed URL
|
|
307
|
+
upload_response = requests.put(
|
|
308
|
+
signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
|
|
309
|
+
)
|
|
427
310
|
|
|
428
311
|
if not upload_response.ok:
|
|
429
312
|
raise Exception(
|
|
430
|
-
f"Failed to upload
|
|
313
|
+
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
431
314
|
)
|
|
432
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
|
+
|
|
433
323
|
|
|
434
324
|
def _determine_content_type(file_path: str) -> str:
|
|
435
325
|
"""
|
figpack/core/config.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
|