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.
- {figpack-0.2.36/figpack.egg-info → figpack-0.2.38}/PKG-INFO +2 -2
- {figpack-0.2.36 → figpack-0.2.38}/README.md +1 -1
- {figpack-0.2.36 → figpack-0.2.38}/figpack/__init__.py +1 -1
- {figpack-0.2.36 → figpack-0.2.38}/figpack/cli.py +150 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_upload_bundle.py +47 -41
- 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
- {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/index.html +1 -1
- {figpack-0.2.36 → figpack-0.2.38/figpack.egg-info}/PKG-INFO +2 -2
- {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/SOURCES.txt +1 -1
- {figpack-0.2.36 → figpack-0.2.38}/pyproject.toml +1 -1
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_upload_bundle.py +1 -25
- {figpack-0.2.36 → figpack-0.2.38}/LICENSE +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/MANIFEST.in +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/__init__.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_bundle_utils.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_file_handler.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_save_figure.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_server_manager.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_show_view.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/_view_figure.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/config.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/extension_view.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/figpack_extension.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/figpack_view.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/core/zarr.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/extensions.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/assets/index-V5m_wCvw.css +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/figpack-figure-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Box.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/CaptionedView.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/DataFrame.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Gallery.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/GalleryItem.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Iframe.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Image.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/LayoutItem.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Markdown.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MatplotlibFigure.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MountainLayout.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MountainLayoutItem.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/MultiChannelTimeseries.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/PlotlyExtension.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/__init__.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/_plotly_extension.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/PlotlyExtension/plotly_view.js +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Spectrogram.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/Splitter.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TabLayout.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TabLayoutItem.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/TimeseriesGraph.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack/views/__init__.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/requires.txt +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/setup.cfg +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_box.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_cli.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_core.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_dataframe.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_extension_system.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_figpack_view.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_file_handler.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_gallery.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_image.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_markdown.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_matplotlib_figure.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_multichannel_timeseries.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_plotly_figure.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_server_manager.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_spectrogram.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_splitter.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_tablayout.py +0 -0
- {figpack-0.2.36 → figpack-0.2.38}/tests/test_timeseries_graph.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|