figpack 0.2.36__tar.gz → 0.2.38__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.

Files changed (75) hide show
  1. {figpack-0.2.36/figpack.egg-info → figpack-0.2.38}/PKG-INFO +2 -2
  2. {figpack-0.2.36 → figpack-0.2.38}/README.md +1 -1
  3. {figpack-0.2.36 → figpack-0.2.38}/figpack/__init__.py +1 -1
  4. {figpack-0.2.36 → figpack-0.2.38}/figpack/cli.py +150 -0
  5. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_upload_bundle.py +47 -41
  6. figpack-0.2.36/figpack/figpack-figure-dist/assets/index-Bt8OPETP.js → figpack-0.2.38/figpack/figpack-figure-dist/assets/index-DsU-DhF6.js +37 -37
  7. {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/index.html +1 -1
  8. {figpack-0.2.36 → figpack-0.2.38/figpack.egg-info}/PKG-INFO +2 -2
  9. {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/SOURCES.txt +1 -1
  10. {figpack-0.2.36 → figpack-0.2.38}/pyproject.toml +1 -1
  11. {figpack-0.2.36 → figpack-0.2.38}/tests/test_upload_bundle.py +1 -25
  12. {figpack-0.2.36 → figpack-0.2.38}/LICENSE +0 -0
  13. {figpack-0.2.36 → figpack-0.2.38}/MANIFEST.in +0 -0
  14. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/__init__.py +0 -0
  15. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_bundle_utils.py +0 -0
  16. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_file_handler.py +0 -0
  17. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_save_figure.py +0 -0
  18. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_server_manager.py +0 -0
  19. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_show_view.py +0 -0
  20. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_view_figure.py +0 -0
  21. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/config.py +0 -0
  22. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/extension_view.py +0 -0
  23. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/figpack_extension.py +0 -0
  24. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/figpack_view.py +0 -0
  25. {figpack-0.2.36 → figpack-0.2.38}/figpack/core/zarr.py +0 -0
  26. {figpack-0.2.36 → figpack-0.2.38}/figpack/extensions.py +0 -0
  27. {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/assets/index-V5m_wCvw.css +0 -0
  28. {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
  29. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Box.py +0 -0
  30. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/CaptionedView.py +0 -0
  31. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/DataFrame.py +0 -0
  32. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Gallery.py +0 -0
  33. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/GalleryItem.py +0 -0
  34. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Iframe.py +0 -0
  35. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Image.py +0 -0
  36. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/LayoutItem.py +0 -0
  37. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Markdown.py +0 -0
  38. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MatplotlibFigure.py +0 -0
  39. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MountainLayout.py +0 -0
  40. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MountainLayoutItem.py +0 -0
  41. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MultiChannelTimeseries.py +0 -0
  42. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/PlotlyExtension.py +0 -0
  43. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/__init__.py +0 -0
  44. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/_plotly_extension.py +0 -0
  45. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/plotly_view.js +0 -0
  46. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Spectrogram.py +0 -0
  47. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Splitter.py +0 -0
  48. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TabLayout.py +0 -0
  49. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TabLayoutItem.py +0 -0
  50. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TimeseriesGraph.py +0 -0
  51. {figpack-0.2.36 → figpack-0.2.38}/figpack/views/__init__.py +0 -0
  52. {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/dependency_links.txt +0 -0
  53. {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/entry_points.txt +0 -0
  54. {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/requires.txt +0 -0
  55. {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/top_level.txt +0 -0
  56. {figpack-0.2.36 → figpack-0.2.38}/setup.cfg +0 -0
  57. {figpack-0.2.36 → figpack-0.2.38}/tests/test_box.py +0 -0
  58. {figpack-0.2.36 → figpack-0.2.38}/tests/test_cli.py +0 -0
  59. {figpack-0.2.36 → figpack-0.2.38}/tests/test_core.py +0 -0
  60. {figpack-0.2.36 → figpack-0.2.38}/tests/test_dataframe.py +0 -0
  61. {figpack-0.2.36 → figpack-0.2.38}/tests/test_extension_system.py +0 -0
  62. {figpack-0.2.36 → figpack-0.2.38}/tests/test_figpack_view.py +0 -0
  63. {figpack-0.2.36 → figpack-0.2.38}/tests/test_file_handler.py +0 -0
  64. {figpack-0.2.36 → figpack-0.2.38}/tests/test_gallery.py +0 -0
  65. {figpack-0.2.36 → figpack-0.2.38}/tests/test_image.py +0 -0
  66. {figpack-0.2.36 → figpack-0.2.38}/tests/test_markdown.py +0 -0
  67. {figpack-0.2.36 → figpack-0.2.38}/tests/test_matplotlib_figure.py +0 -0
  68. {figpack-0.2.36 → figpack-0.2.38}/tests/test_multichannel_timeseries.py +0 -0
  69. {figpack-0.2.36 → figpack-0.2.38}/tests/test_plotly_figure.py +0 -0
  70. {figpack-0.2.36 → figpack-0.2.38}/tests/test_server_manager.py +0 -0
  71. {figpack-0.2.36 → figpack-0.2.38}/tests/test_spectrogram.py +0 -0
  72. {figpack-0.2.36 → figpack-0.2.38}/tests/test_splitter.py +0 -0
  73. {figpack-0.2.36 → figpack-0.2.38}/tests/test_tablayout.py +0 -0
  74. {figpack-0.2.36 → figpack-0.2.38}/tests/test_timeseries_graph.py +0 -0
  75. {figpack-0.2.36 → figpack-0.2.38}/tests/test_view_figure.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: figpack
3
- Version: 0.2.36
3
+ Version: 0.2.38
4
4
  Summary: A Python package for creating shareable, interactive visualizations in the browser
5
5
  Author-email: Jeremy Magland <jmagland@flatironinstitute.org>
6
6
  License: Apache-2.0
@@ -133,7 +133,7 @@ If you use figpack in your research, please cite it:
133
133
 
134
134
  Or in APA format:
135
135
 
136
- > Magland, J. (2025). figpack (Version 0.2.36) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.17419621
136
+ > Magland, J. (2025). figpack (Version 0.2.38) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.17419621
137
137
 
138
138
  ## Contributing
139
139
 
@@ -57,7 +57,7 @@ If you use figpack in your research, please cite it:
57
57
 
58
58
  Or in APA format:
59
59
 
60
- > Magland, J. (2025). figpack (Version 0.2.36) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.17419621
60
+ > Magland, J. (2025). figpack (Version 0.2.38) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.17419621
61
61
 
62
62
  ## Contributing
63
63
 
@@ -2,7 +2,7 @@
2
2
  figpack - A Python package for creating shareable, interactive visualizations in the browser
3
3
  """
4
4
 
5
- __version__ = "0.2.36"
5
+ __version__ = "0.2.38"
6
6
 
7
7
  from .cli import view_figure
8
8
  from .core import FigpackView, FigpackExtension, ExtensionView
@@ -18,6 +18,7 @@ import requests
18
18
  from . import __version__
19
19
  from .core._server_manager import CORSRequestHandler
20
20
  from .core._view_figure import serve_files, view_figure
21
+ from .core._upload_bundle import _upload_bundle, get_figure_by_source_url
21
22
  from .extensions import ExtensionManager
22
23
 
23
24
  MAX_WORKERS_FOR_DOWNLOAD = 16
@@ -249,6 +250,130 @@ def handle_extensions_command(args):
249
250
  print("Use 'figpack extensions <command> --help' for more information.")
250
251
 
251
252
 
253
+ def handle_upload_from_source_url(args) -> None:
254
+ """
255
+ Handle the upload-from-source-url command
256
+
257
+ Downloads a tar.gz/tgz file from a URL, extracts it, and uploads it as a new figure
258
+ with the source URL set.
259
+ """
260
+ import os
261
+
262
+ source_url = args.source_url
263
+ title = args.title if hasattr(args, "title") else None
264
+
265
+ # Get API key from environment variable
266
+ api_key = os.environ.get("FIGPACK_API_KEY")
267
+ if not api_key:
268
+ print(
269
+ "Error: FIGPACK_API_KEY environment variable must be set to upload figures."
270
+ )
271
+ sys.exit(1)
272
+
273
+ # Validate URL format
274
+ if not (source_url.endswith(".tar.gz") or source_url.endswith(".tgz")):
275
+ print(f"Error: Source URL must point to a .tar.gz or .tgz file: {source_url}")
276
+ sys.exit(1)
277
+
278
+ print(f"Downloading archive from: {source_url}")
279
+
280
+ try:
281
+ # Download the archive
282
+ response = requests.get(source_url, timeout=120, stream=True)
283
+ response.raise_for_status()
284
+
285
+ # Create temporary file for the archive
286
+ with tempfile.NamedTemporaryFile(
287
+ suffix=".tar.gz", delete=False
288
+ ) as temp_archive:
289
+ archive_path = temp_archive.name
290
+
291
+ # Download with progress indication
292
+ total_size = int(response.headers.get("content-length", 0))
293
+ downloaded_size = 0
294
+
295
+ for chunk in response.iter_content(chunk_size=8192):
296
+ if chunk:
297
+ temp_archive.write(chunk)
298
+ downloaded_size += len(chunk)
299
+ if total_size > 0:
300
+ progress = (downloaded_size / total_size) * 100
301
+ print(
302
+ f"Downloaded: {downloaded_size / (1024*1024):.2f} MB ({progress:.1f}%)",
303
+ end="\r",
304
+ )
305
+
306
+ if total_size > 0:
307
+ print() # New line after progress
308
+ print(f"Download complete: {downloaded_size / (1024*1024):.2f} MB")
309
+
310
+ # Extract archive to temporary directory
311
+ print("Extracting archive...")
312
+ with tempfile.TemporaryDirectory() as extract_dir:
313
+ extract_path = pathlib.Path(extract_dir)
314
+
315
+ try:
316
+ with tarfile.open(archive_path, "r:gz") as tar:
317
+ tar.extractall(path=extract_path)
318
+ print(f"Extracted to temporary directory")
319
+ except Exception as e:
320
+ print(f"Error: Failed to extract archive: {e}")
321
+ sys.exit(1)
322
+ finally:
323
+ # Clean up downloaded archive
324
+ import os
325
+
326
+ try:
327
+ os.unlink(archive_path)
328
+ except Exception:
329
+ pass
330
+
331
+ # Upload the extracted files
332
+ print(f"Uploading figure with source URL: {source_url}")
333
+ try:
334
+ figure_url = _upload_bundle(
335
+ str(extract_path),
336
+ api_key=api_key,
337
+ title=title,
338
+ source_url=source_url,
339
+ )
340
+ print(f"\nFigure uploaded successfully!")
341
+ print(f"Figure URL: {figure_url}")
342
+ except Exception as e:
343
+ print(f"Error: Failed to upload figure: {e}")
344
+ sys.exit(1)
345
+
346
+ except requests.exceptions.RequestException as e:
347
+ print(f"Error: Failed to download archive from {source_url}: {e}")
348
+ sys.exit(1)
349
+ except Exception as e:
350
+ print(f"Error: {e}")
351
+ sys.exit(1)
352
+
353
+
354
+ def handle_find_by_source_url(args) -> None:
355
+ """
356
+ Handle the find-by-source-url command
357
+
358
+ Queries the API for a figure URL by its source URL.
359
+ """
360
+ source_url = args.source_url
361
+
362
+ print(f"Querying for figure with source URL: {source_url}")
363
+
364
+ try:
365
+ figure_url = get_figure_by_source_url(source_url)
366
+
367
+ if figure_url:
368
+ print(f"Figure found: {figure_url}")
369
+ else:
370
+ print(f"No figure found with source URL: {source_url}")
371
+ sys.exit(1)
372
+ except Exception as e:
373
+ print(f"Error: {e}")
374
+ sys.exit(1)
375
+
376
+
252
377
  def download_and_view_archive(url: str, port: int = None) -> None:
253
378
  """
254
379
  Download a tar.gz/tgz archive from a URL and view it
@@ -370,6 +495,27 @@ def main():
370
495
  "extensions", nargs="+", help="Extension package names to uninstall"
371
496
  )
372
497
 
498
+ # Upload from URL command
499
+ upload_from_source_url_parser = subparsers.add_parser(
500
+ "upload-from-source-url",
501
+ help="Download a tar.gz/tgz file and upload it as a new figure",
502
+ )
503
+ upload_from_source_url_parser.add_argument(
504
+ "source_url",
505
+ help="URL to the tar.gz or tgz file (will be set as the figure's source URL)",
506
+ )
507
+ upload_from_source_url_parser.add_argument(
508
+ "--title", help="Optional title for the figure"
509
+ )
510
+
511
+ # Find by source URL command
512
+ find_by_source_url_parser = subparsers.add_parser(
513
+ "find-by-source-url", help="Get the figure URL for a given source URL"
514
+ )
515
+ find_by_source_url_parser.add_argument(
516
+ "source_url", help="The source URL to search for"
517
+ )
518
+
373
519
  args = parser.parse_args()
374
520
 
375
521
  if args.command == "download":
@@ -382,6 +528,10 @@ def main():
382
528
  view_figure(args.archive, port=args.port)
383
529
  elif args.command == "extensions":
384
530
  handle_extensions_command(args)
531
+ elif args.command == "upload-from-source-url":
532
+ handle_upload_from_source_url(args)
533
+ elif args.command == "find-by-source-url":
534
+ handle_find_by_source_url(args)
385
535
  else:
386
536
  parser.print_help()
387
537
 
@@ -122,56 +122,24 @@ def _upload_single_file_with_signed_url(
122
122
  MAX_WORKERS_FOR_UPLOAD = 16
123
123
 
124
124
 
125
- def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
126
- """
127
- Compute a deterministic figure ID based on SHA1 hashes of all files
128
-
129
- Returns:
130
- str: 40-character SHA1 hash representing the content of all files
131
- """
132
- file_hashes = []
133
-
134
- # Collect all files and their hashes
135
- for file_path in sorted(tmpdir_path.rglob("*")):
136
- if file_path.is_file():
137
- relative_path = file_path.relative_to(tmpdir_path)
138
-
139
- # Compute SHA1 hash of file content
140
- sha1_hash = hashlib.sha1()
141
- with open(file_path, "rb") as f:
142
- for chunk in iter(lambda: f.read(4096), b""):
143
- sha1_hash.update(chunk)
144
-
145
- # Include both the relative path and content hash to ensure uniqueness
146
- file_info = f"{relative_path}:{sha1_hash.hexdigest()}"
147
- file_hashes.append(file_info)
148
-
149
- # Create final hash from all file hashes
150
- combined_hash = hashlib.sha1()
151
- for file_hash in file_hashes:
152
- combined_hash.update(file_hash.encode("utf-8"))
153
-
154
- return combined_hash.hexdigest()
155
-
156
-
157
125
  def _create_or_get_figure(
158
- figure_hash: str,
159
126
  api_key: Optional[str],
160
127
  total_files: Optional[int] = None,
161
128
  total_size: Optional[int] = None,
162
129
  title: Optional[str] = None,
163
130
  ephemeral: bool = False,
131
+ source_url: Optional[str] = None,
164
132
  ) -> dict:
165
133
  """
166
134
  Create a new figure or get existing figure information
167
135
 
168
136
  Args:
169
- figure_hash: The hash of the figure
170
137
  api_key: The API key for authentication (required for non-ephemeral)
171
138
  total_files: Optional total number of files
172
139
  total_size: Optional total size of files
173
140
  title: Optional title for the figure
174
141
  ephemeral: Whether to create an ephemeral figure
142
+ source_url: Optional source URL for the figure (must be unique)
175
143
 
176
144
  Returns:
177
145
  dict: Figure information from the API
@@ -181,7 +149,6 @@ def _create_or_get_figure(
181
149
  raise ValueError("API key is required for non-ephemeral figures")
182
150
 
183
151
  payload: dict[str, Union[str, int]] = {
184
- "figureHash": figure_hash,
185
152
  "figpackVersion": __version__,
186
153
  "bucket": FIGPACK_BUCKET,
187
154
  }
@@ -198,6 +165,8 @@ def _create_or_get_figure(
198
165
  payload["title"] = title
199
166
  if ephemeral:
200
167
  payload["ephemeral"] = True
168
+ if source_url is not None:
169
+ payload["sourceUrl"] = source_url
201
170
 
202
171
  # Use the same endpoint for both regular and ephemeral figures
203
172
  response = requests.post(f"{FIGPACK_API_BASE_URL}/api/figures/create", json=payload)
@@ -208,12 +177,12 @@ def _create_or_get_figure(
208
177
  error_msg = error_data.get("message", "Unknown error")
209
178
  except:
210
179
  error_msg = f"HTTP {response.status_code}"
211
- raise Exception(f"Failed to create figure {figure_hash}: {error_msg}")
180
+ raise Exception(f"Failed to create figure: {error_msg}")
212
181
 
213
182
  response_data = response.json()
214
183
  if not response_data.get("success"):
215
184
  raise Exception(
216
- f"Failed to create figure {figure_hash}: {response_data.get('message', 'Unknown error')}"
185
+ f"Failed to create figure: {response_data.get('message', 'Unknown error')}"
217
186
  )
218
187
 
219
188
  return response_data
@@ -258,6 +227,7 @@ def _upload_bundle(
258
227
  title: Optional[str] = None,
259
228
  ephemeral: bool = False,
260
229
  use_consolidated_metadata_only: bool = False,
230
+ source_url: Optional[str] = None,
261
231
  ) -> str:
262
232
  """
263
233
  Upload the prepared bundle to the cloud using the new database-driven approach
@@ -269,12 +239,10 @@ def _upload_bundle(
269
239
  ephemeral: Whether to create an ephemeral figure
270
240
  use_consolidated_metadata_only: If True, excludes individual zarr metadata files
271
241
  (.zgroup, .zarray, .zattrs) since they are included in .zmetadata
242
+ source_url: Optional source URL for the figure (must be unique)
272
243
  """
273
244
  tmpdir_path = pathlib.Path(tmpdir)
274
245
 
275
- # Compute deterministic figure ID based on file contents
276
- figure_hash = _compute_deterministic_figure_hash(tmpdir_path)
277
-
278
246
  # Collect all files to upload
279
247
  all_files = []
280
248
  for file_path in tmpdir_path.rglob("*"):
@@ -295,7 +263,12 @@ def _upload_bundle(
295
263
 
296
264
  # Find available figure ID and create/get figure in database with metadata
297
265
  result = _create_or_get_figure(
298
- figure_hash, api_key, total_files, total_size, title=title, ephemeral=ephemeral
266
+ api_key,
267
+ total_files,
268
+ total_size,
269
+ title=title,
270
+ ephemeral=ephemeral,
271
+ source_url=source_url,
299
272
  )
300
273
  figure_info = result.get("figure", {})
301
274
  figure_url = figure_info.get("figureUrl")
@@ -430,6 +403,39 @@ def _upload_bundle(
430
403
  return figure_url
431
404
 
432
405
 
406
+ def get_figure_by_source_url(source_url: str) -> Optional[str]:
407
+ """
408
+ Query the API for a figure URL by its source URL
409
+
410
+ Args:
411
+ source_url: The source URL to search for
412
+
413
+ Returns:
414
+ Optional[str]: The figure URL if found, None otherwise
415
+ """
416
+ payload = {"sourceUrl": source_url}
417
+
418
+ response = requests.post(
419
+ f"{FIGPACK_API_BASE_URL}/api/figures/find-by-source-url", json=payload
420
+ )
421
+
422
+ if not response.ok:
423
+ if response.status_code == 404:
424
+ return None
425
+ try:
426
+ error_data = response.json()
427
+ error_msg = error_data.get("message", "Unknown error")
428
+ except:
429
+ error_msg = f"HTTP {response.status_code}"
430
+ raise Exception(f"Failed to query figure by source URL: {error_msg}")
431
+
432
+ response_data = response.json()
433
+ if not response_data.get("success"):
434
+ return None
435
+
436
+ return response_data.get("figureUrl")
437
+
438
+
433
439
  def _determine_content_type(file_path: str) -> str:
434
440
  """
435
441
  Determine content type for upload based on file extension