figpack 0.1.6__tar.gz → 0.2.1__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.6/figpack.egg-info → figpack-0.2.1}/PKG-INFO +1 -1
- {figpack-0.1.6 → figpack-0.2.1}/figpack/__init__.py +1 -1
- {figpack-0.1.6 → figpack-0.2.1}/figpack/core/_show_view.py +1 -1
- {figpack-0.1.6 → figpack-0.2.1}/figpack/core/_upload_bundle.py +131 -76
- figpack-0.2.1/figpack/core/config.py +5 -0
- figpack-0.1.6/figpack/figpack-gui-dist/assets/index-DaeClgi6.js → figpack-0.2.1/figpack/figpack-gui-dist/assets/index-BqYF6BN-.js +91 -91
- figpack-0.2.1/figpack/figpack-gui-dist/assets/index-Cmae55E4.css +1 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/figpack-gui-dist/index.html +2 -2
- {figpack-0.1.6 → figpack-0.2.1/figpack.egg-info}/PKG-INFO +1 -1
- {figpack-0.1.6 → figpack-0.2.1}/figpack.egg-info/SOURCES.txt +3 -4
- {figpack-0.1.6 → figpack-0.2.1}/pyproject.toml +1 -1
- figpack-0.1.6/figpack/core/config.py +0 -1
- figpack-0.1.6/figpack/figpack-gui-dist/assets/index-BDa2iJW9.css +0 -1
- figpack-0.1.6/tests/test_upload_bundle.py +0 -679
- {figpack-0.1.6 → figpack-0.2.1}/LICENSE +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/MANIFEST.in +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/README.md +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/cli.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/core/__init__.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/core/_bundle_utils.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/core/figpack_view.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/figpack-gui-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/__init__.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/AutocorrelogramItem.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/Autocorrelograms.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/CrossCorrelograms.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/UnitSimilarityScore.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/UnitsTable.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/UnitsTableColumn.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/UnitsTableRow.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/spike_sorting/views/__init__.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/Box.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/Image.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/LayoutItem.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/Markdown.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/MatplotlibFigure.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/MultiChannelTimeseries.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/PlotlyFigure.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/Splitter.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/TabLayout.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/TabLayoutItem.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/TimeseriesGraph.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack/views/__init__.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack.egg-info/requires.txt +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/setup.cfg +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_box.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_cli.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_core.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_figpack_view.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_image.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_markdown.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_matplotlib_figure.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_multichannel_timeseries.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_plotly_figure.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_show_view.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_spike_sorting_correlograms.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_splitter.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_tablayout.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_timeseries_graph.py +0 -0
- {figpack-0.1.6 → figpack-0.2.1}/tests/test_units_table.py +0 -0
|
@@ -16,23 +16,28 @@ from .config import FIGPACK_API_BASE_URL
|
|
|
16
16
|
thisdir = pathlib.Path(__file__).parent.resolve()
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
figure_url: str, relative_path: str, file_path: pathlib.Path, api_key: str
|
|
21
|
-
) -> str:
|
|
19
|
+
def _get_batch_signed_urls(figure_url: str, files_batch: list, api_key: str) -> dict:
|
|
22
20
|
"""
|
|
23
|
-
|
|
21
|
+
Get signed URLs for a batch of files
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
figure_url: The figure URL
|
|
25
|
+
files_batch: List of tuples (relative_path, file_path)
|
|
26
|
+
api_key: API key for authentication
|
|
24
27
|
|
|
25
28
|
Returns:
|
|
26
|
-
|
|
29
|
+
dict: Mapping of relative_path to signed_url
|
|
27
30
|
"""
|
|
28
|
-
|
|
31
|
+
# Prepare batch request
|
|
32
|
+
files_data = []
|
|
33
|
+
for relative_path, file_path in files_batch:
|
|
34
|
+
file_size = file_path.stat().st_size
|
|
35
|
+
files_data.append({"relativePath": relative_path, "size": file_size})
|
|
29
36
|
|
|
30
|
-
# Get signed URL
|
|
31
37
|
payload = {
|
|
32
38
|
"figureUrl": figure_url,
|
|
33
|
-
"
|
|
39
|
+
"files": files_data,
|
|
34
40
|
"apiKey": api_key,
|
|
35
|
-
"size": file_size,
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/upload", json=payload)
|
|
@@ -43,18 +48,35 @@ def _upload_single_file(
|
|
|
43
48
|
error_msg = error_data.get("message", "Unknown error")
|
|
44
49
|
except:
|
|
45
50
|
error_msg = f"HTTP {response.status_code}"
|
|
46
|
-
raise Exception(f"Failed to get signed
|
|
51
|
+
raise Exception(f"Failed to get signed URLs for batch: {error_msg}")
|
|
47
52
|
|
|
48
53
|
response_data = response.json()
|
|
49
54
|
if not response_data.get("success"):
|
|
50
55
|
raise Exception(
|
|
51
|
-
f"Failed to get signed
|
|
56
|
+
f"Failed to get signed URLs for batch: {response_data.get('message', 'Unknown error')}"
|
|
52
57
|
)
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
if not
|
|
56
|
-
raise Exception(
|
|
59
|
+
signed_urls_data = response_data.get("signedUrls", [])
|
|
60
|
+
if not signed_urls_data:
|
|
61
|
+
raise Exception("No signed URLs returned for batch")
|
|
62
|
+
|
|
63
|
+
# Convert to mapping
|
|
64
|
+
signed_urls_map = {}
|
|
65
|
+
for item in signed_urls_data:
|
|
66
|
+
signed_urls_map[item["relativePath"]] = item["signedUrl"]
|
|
67
|
+
|
|
68
|
+
return signed_urls_map
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _upload_single_file_with_signed_url(
|
|
72
|
+
relative_path: str, file_path: pathlib.Path, signed_url: str
|
|
73
|
+
) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Upload a single file using a pre-obtained signed URL
|
|
57
76
|
|
|
77
|
+
Returns:
|
|
78
|
+
str: The relative path of the uploaded file
|
|
79
|
+
"""
|
|
58
80
|
# Upload file to signed URL
|
|
59
81
|
content_type = _determine_content_type(relative_path)
|
|
60
82
|
with open(file_path, "rb") as f:
|
|
@@ -106,11 +128,22 @@ def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
|
|
|
106
128
|
|
|
107
129
|
|
|
108
130
|
def _create_or_get_figure(
|
|
109
|
-
figure_hash: str,
|
|
131
|
+
figure_hash: str,
|
|
132
|
+
api_key: str,
|
|
133
|
+
total_files: int = None,
|
|
134
|
+
total_size: int = None,
|
|
135
|
+
title: str = None,
|
|
110
136
|
) -> dict:
|
|
111
137
|
"""
|
|
112
138
|
Create a new figure or get existing figure information
|
|
113
139
|
|
|
140
|
+
Args:
|
|
141
|
+
figure_hash: The hash of the figure
|
|
142
|
+
api_key: The API key for authentication
|
|
143
|
+
total_files: Optional total number of files
|
|
144
|
+
total_size: Optional total size of files
|
|
145
|
+
title: Optional title for the figure
|
|
146
|
+
|
|
114
147
|
Returns:
|
|
115
148
|
dict: Figure information from the API
|
|
116
149
|
"""
|
|
@@ -124,6 +157,8 @@ def _create_or_get_figure(
|
|
|
124
157
|
payload["totalFiles"] = total_files
|
|
125
158
|
if total_size is not None:
|
|
126
159
|
payload["totalSize"] = total_size
|
|
160
|
+
if title is not None:
|
|
161
|
+
payload["title"] = title
|
|
127
162
|
|
|
128
163
|
response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
|
|
129
164
|
|
|
@@ -177,7 +212,7 @@ def _finalize_figure(figure_url: str, api_key: str) -> dict:
|
|
|
177
212
|
return response_data
|
|
178
213
|
|
|
179
214
|
|
|
180
|
-
def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
215
|
+
def _upload_bundle(tmpdir: str, api_key: str, title: str = None) -> str:
|
|
181
216
|
"""
|
|
182
217
|
Upload the prepared bundle to the cloud using the new database-driven approach
|
|
183
218
|
"""
|
|
@@ -203,7 +238,9 @@ def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
|
203
238
|
)
|
|
204
239
|
|
|
205
240
|
# Find available figure ID and create/get figure in database with metadata
|
|
206
|
-
result = _create_or_get_figure(
|
|
241
|
+
result = _create_or_get_figure(
|
|
242
|
+
figure_hash, api_key, total_files, total_size, title=title
|
|
243
|
+
)
|
|
207
244
|
figure_info = result.get("figure", {})
|
|
208
245
|
figure_url = figure_info.get("figureUrl")
|
|
209
246
|
|
|
@@ -220,39 +257,63 @@ def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
|
220
257
|
print("No files to upload")
|
|
221
258
|
else:
|
|
222
259
|
print(
|
|
223
|
-
f"Uploading {total_files_to_upload} files with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads..."
|
|
260
|
+
f"Uploading {total_files_to_upload} files in batches of 20 with up to {MAX_WORKERS_FOR_UPLOAD} concurrent uploads per batch..."
|
|
224
261
|
)
|
|
225
262
|
|
|
226
263
|
# Thread-safe progress tracking
|
|
227
264
|
uploaded_count = 0
|
|
228
265
|
count_lock = threading.Lock()
|
|
229
266
|
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
for
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
# Process files in batches of 20
|
|
268
|
+
batch_size = 20
|
|
269
|
+
for i in range(0, total_files_to_upload, batch_size):
|
|
270
|
+
batch = files_to_upload[i : i + batch_size]
|
|
271
|
+
batch_num = i // batch_size + 1
|
|
272
|
+
total_batches = (total_files_to_upload + batch_size - 1) // batch_size
|
|
273
|
+
|
|
274
|
+
print(
|
|
275
|
+
f"Processing batch {batch_num}/{total_batches} ({len(batch)} files)..."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Get signed URLs for this batch
|
|
279
|
+
try:
|
|
280
|
+
signed_urls_map = _get_batch_signed_urls(figure_url, batch, api_key)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"Failed to get signed URLs for batch {batch_num}: {e}")
|
|
283
|
+
raise
|
|
284
|
+
|
|
285
|
+
# Upload files in this batch in parallel
|
|
286
|
+
with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
|
|
287
|
+
# Submit upload tasks for this batch
|
|
288
|
+
future_to_file = {}
|
|
289
|
+
for rel_path, file_path in batch:
|
|
290
|
+
if rel_path in signed_urls_map:
|
|
291
|
+
future = executor.submit(
|
|
292
|
+
_upload_single_file_with_signed_url,
|
|
293
|
+
rel_path,
|
|
294
|
+
file_path,
|
|
295
|
+
signed_urls_map[rel_path],
|
|
251
296
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
297
|
+
future_to_file[future] = rel_path
|
|
298
|
+
else:
|
|
299
|
+
print(f"Warning: No signed URL found for {rel_path}")
|
|
300
|
+
|
|
301
|
+
# Process completed uploads for this batch
|
|
302
|
+
for future in as_completed(future_to_file):
|
|
303
|
+
relative_path = future_to_file[future]
|
|
304
|
+
try:
|
|
305
|
+
future.result() # This will raise any exception that occurred during upload
|
|
306
|
+
|
|
307
|
+
# Thread-safe progress update
|
|
308
|
+
with count_lock:
|
|
309
|
+
uploaded_count += 1
|
|
310
|
+
print(
|
|
311
|
+
f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
print(f"Failed to upload {relative_path}: {e}")
|
|
316
|
+
raise # Re-raise the exception to stop the upload process
|
|
256
317
|
|
|
257
318
|
# Create manifest for finalization
|
|
258
319
|
print("Creating manifest...")
|
|
@@ -270,49 +331,43 @@ def _upload_bundle(tmpdir: str, api_key: str) -> str:
|
|
|
270
331
|
|
|
271
332
|
print(f"Total size: {manifest['total_size'] / (1024 * 1024):.2f} MB")
|
|
272
333
|
|
|
273
|
-
# Upload manifest.json
|
|
334
|
+
# Upload manifest.json using batch API
|
|
274
335
|
print("Uploading manifest.json...")
|
|
275
336
|
manifest_content = json.dumps(manifest, indent=2)
|
|
276
337
|
manifest_size = len(manifest_content.encode("utf-8"))
|
|
277
338
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"relativePath": "manifest.json",
|
|
281
|
-
"apiKey": api_key,
|
|
282
|
-
"size": manifest_size,
|
|
283
|
-
}
|
|
339
|
+
# Create a temporary file for the manifest
|
|
340
|
+
import tempfile
|
|
284
341
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
)
|
|
342
|
+
with tempfile.NamedTemporaryFile(
|
|
343
|
+
mode="w", suffix=".json", delete=False
|
|
344
|
+
) as temp_file:
|
|
345
|
+
temp_file.write(manifest_content)
|
|
346
|
+
temp_file_path = pathlib.Path(temp_file.name)
|
|
301
347
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
try:
|
|
349
|
+
# Use batch API for manifest
|
|
350
|
+
manifest_batch = [("manifest.json", temp_file_path)]
|
|
351
|
+
signed_urls_map = _get_batch_signed_urls(figure_url, manifest_batch, api_key)
|
|
305
352
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
signed_url, data=manifest_content, headers={"Content-Type": "application/json"}
|
|
309
|
-
)
|
|
353
|
+
if "manifest.json" not in signed_urls_map:
|
|
354
|
+
raise Exception("No signed URL returned for manifest.json")
|
|
310
355
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
356
|
+
# Upload manifest using signed URL
|
|
357
|
+
upload_response = requests.put(
|
|
358
|
+
signed_urls_map["manifest.json"],
|
|
359
|
+
data=manifest_content,
|
|
360
|
+
headers={"Content-Type": "application/json"},
|
|
314
361
|
)
|
|
315
362
|
|
|
363
|
+
if not upload_response.ok:
|
|
364
|
+
raise Exception(
|
|
365
|
+
f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
|
|
366
|
+
)
|
|
367
|
+
finally:
|
|
368
|
+
# Clean up temporary file
|
|
369
|
+
temp_file_path.unlink(missing_ok=True)
|
|
370
|
+
|
|
316
371
|
# Finalize the figure upload
|
|
317
372
|
print("Finalizing figure...")
|
|
318
373
|
_finalize_figure(figure_url, api_key)
|