figpack 0.1.6__py3-none-any.whl → 0.2.1__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 CHANGED
@@ -2,4 +2,4 @@
2
2
  figpack - A Python package for creating shareable, interactive visualizations in the browser
3
3
  """
4
4
 
5
- __version__ = "0.1.6"
5
+ __version__ = "0.2.1"
@@ -36,7 +36,7 @@ def _show_view(
36
36
 
37
37
  # Upload the bundle
38
38
  print("Starting upload...")
39
- figure_url = _upload_bundle(tmpdir, api_key)
39
+ figure_url = _upload_bundle(tmpdir, api_key, title=title)
40
40
 
41
41
  if open_in_browser:
42
42
  webbrowser.open(figure_url)
@@ -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 _upload_single_file(
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
- Worker function to upload a single file using signed URL
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
- str: The relative path of the uploaded file
29
+ dict: Mapping of relative_path to signed_url
27
30
  """
28
- file_size = file_path.stat().st_size
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
- "relativePath": relative_path,
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 URL for {relative_path}: {error_msg}")
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 URL for {relative_path}: {response_data.get('message', 'Unknown error')}"
56
+ f"Failed to get signed URLs for batch: {response_data.get('message', 'Unknown error')}"
52
57
  )
53
58
 
54
- signed_url = response_data.get("signedUrl")
55
- if not signed_url:
56
- raise Exception(f"No signed URL returned for {relative_path}")
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, api_key: str, total_files: int = None, total_size: int = None
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(figure_hash, api_key, total_files, total_size)
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
- # Upload files in parallel with concurrent uploads
231
- with ThreadPoolExecutor(max_workers=MAX_WORKERS_FOR_UPLOAD) as executor:
232
- # Submit all upload tasks
233
- future_to_file = {
234
- executor.submit(
235
- _upload_single_file, figure_url, rel_path, file_path, api_key
236
- ): rel_path
237
- for rel_path, file_path in files_to_upload
238
- }
239
-
240
- # Process completed uploads
241
- for future in as_completed(future_to_file):
242
- relative_path = future_to_file[future]
243
- try:
244
- future.result() # This will raise any exception that occurred during upload
245
-
246
- # Thread-safe progress update
247
- with count_lock:
248
- uploaded_count += 1
249
- print(
250
- f"Uploaded {uploaded_count}/{total_files_to_upload}: {relative_path}"
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
- except Exception as e:
254
- print(f"Failed to upload {relative_path}: {e}")
255
- raise # Re-raise the exception to stop the upload process
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
- manifest_payload = {
279
- "figureUrl": figure_url,
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
- response = requests.post(
286
- f"{FIGPACK_API_BASE_URL}/api/upload", json=manifest_payload
287
- )
288
- if not response.ok:
289
- try:
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
- signed_url = response_data.get("signedUrl")
303
- if not signed_url:
304
- raise Exception("No signed URL returned for manifest.json")
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
- # Upload manifest using signed URL
307
- upload_response = requests.put(
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
- if not upload_response.ok:
312
- raise Exception(
313
- f"Failed to upload manifest.json to signed URL: HTTP {upload_response.status_code}"
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)
figpack/core/config.py CHANGED
@@ -1 +1,5 @@
1
- FIGPACK_API_BASE_URL = "https://figpack-api.vercel.app"
1
+ import os
2
+
3
+ FIGPACK_API_BASE_URL = os.getenv(
4
+ "FIGPACK_API_BASE_URL", "https://figpack-api.vercel.app"
5
+ )