figpack 0.2.34__py3-none-any.whl → 0.2.36__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,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.34"
5
+ __version__ = "0.2.36"
6
6
 
7
7
  from .cli import view_figure
8
8
  from .core import FigpackView, FigpackExtension, ExtensionView
figpack/cli.py CHANGED
@@ -249,6 +249,64 @@ def handle_extensions_command(args):
249
249
  print("Use 'figpack extensions <command> --help' for more information.")
250
250
 
251
251
 
252
+ def download_and_view_archive(url: str, port: int = None) -> None:
253
+ """
254
+ Download a tar.gz/tgz archive from a URL and view it
255
+
256
+ Args:
257
+ url: URL to the tar.gz or tgz file
258
+ port: Optional port number to serve on
259
+ """
260
+ if not (url.endswith(".tar.gz") or url.endswith(".tgz")):
261
+ print(f"Error: URL must point to a .tar.gz or .tgz file: {url}")
262
+ sys.exit(1)
263
+
264
+ print(f"Downloading archive from: {url}")
265
+
266
+ try:
267
+ response = requests.get(url, timeout=60, stream=True)
268
+ response.raise_for_status()
269
+
270
+ # Create a temporary file to store the downloaded archive
271
+ with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as temp_file:
272
+ temp_path = temp_file.name
273
+
274
+ # Download with progress indication
275
+ total_size = int(response.headers.get("content-length", 0))
276
+ downloaded_size = 0
277
+
278
+ for chunk in response.iter_content(chunk_size=8192):
279
+ if chunk:
280
+ temp_file.write(chunk)
281
+ downloaded_size += len(chunk)
282
+ if total_size > 0:
283
+ progress = (downloaded_size / total_size) * 100
284
+ print(
285
+ f"Downloaded: {downloaded_size / (1024*1024):.2f} MB ({progress:.1f}%)",
286
+ end="\r",
287
+ )
288
+
289
+ if total_size > 0:
290
+ print() # New line after progress
291
+ print(f"Download complete: {downloaded_size / (1024*1024):.2f} MB")
292
+
293
+ # Now view the downloaded file
294
+ try:
295
+ view_figure(temp_path, port=port)
296
+ finally:
297
+ # Clean up the temporary file after viewing
298
+ import os
299
+
300
+ try:
301
+ os.unlink(temp_path)
302
+ except Exception:
303
+ pass
304
+
305
+ except requests.exceptions.RequestException as e:
306
+ print(f"Error: Failed to download archive from {url}: {e}")
307
+ sys.exit(1)
308
+
309
+
252
310
  def main():
253
311
  """Main CLI entry point"""
254
312
  parser = argparse.ArgumentParser(
@@ -270,7 +328,7 @@ def main():
270
328
  view_parser = subparsers.add_parser(
271
329
  "view", help="Extract and serve a figure archive locally"
272
330
  )
273
- view_parser.add_argument("archive", help="Path to the tar.gz archive file")
331
+ view_parser.add_argument("archive", help="Path or URL to the tar.gz archive file")
274
332
  view_parser.add_argument(
275
333
  "--port", type=int, help="Port number to serve on (default: auto-select)"
276
334
  )
@@ -317,7 +375,11 @@ def main():
317
375
  if args.command == "download":
318
376
  download_figure(args.figure_url, args.dest)
319
377
  elif args.command == "view":
320
- view_figure(args.archive, port=args.port)
378
+ # Check if archive argument is a URL
379
+ if args.archive.startswith("http://") or args.archive.startswith("https://"):
380
+ download_and_view_archive(args.archive, port=args.port)
381
+ else:
382
+ view_figure(args.archive, port=args.port)
321
383
  elif args.command == "extensions":
322
384
  handle_extensions_command(args)
323
385
  else:
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import pathlib
3
3
  import json
4
- from typing import Set
4
+ from typing import Optional, List
5
5
 
6
6
  import zarr
7
7
 
@@ -14,7 +14,7 @@ thisdir = pathlib.Path(__file__).parent.resolve()
14
14
 
15
15
 
16
16
  def prepare_figure_bundle(
17
- view: FigpackView, tmpdir: str, *, title: str, description: str = None
17
+ view: FigpackView, tmpdir: str, *, title: str, description: Optional[str] = None
18
18
  ) -> None:
19
19
  """
20
20
  Prepare a figure bundle in the specified temporary directory.
@@ -51,8 +51,8 @@ def prepare_figure_bundle(
51
51
  # because we only support version 2 on the frontend right now.
52
52
 
53
53
  if _check_zarr_version() == 3:
54
- old_default_zarr_format = zarr.config.get("default_zarr_format")
55
- zarr.config.set({"default_zarr_format": 2})
54
+ old_default_zarr_format = zarr.config.get("default_zarr_format") # type: ignore
55
+ zarr.config.set({"default_zarr_format": 2}) # type: ignore
56
56
 
57
57
  try:
58
58
  # Write the view data to the Zarr group
@@ -80,7 +80,7 @@ def prepare_figure_bundle(
80
80
  _remove_metadata_files_except_consolidated(pathlib.Path(tmpdir) / "data.zarr")
81
81
  finally:
82
82
  if _check_zarr_version() == 3:
83
- zarr.config.set({"default_zarr_format": old_default_zarr_format})
83
+ zarr.config.set({"default_zarr_format": old_default_zarr_format}) # type: ignore
84
84
 
85
85
 
86
86
  def _remove_metadata_files_except_consolidated(zarr_dir: pathlib.Path) -> None:
@@ -107,7 +107,7 @@ def _remove_metadata_files_except_consolidated(zarr_dir: pathlib.Path) -> None:
107
107
  print(f"Warning: could not remove file {file_path}: {e}")
108
108
 
109
109
 
110
- def _discover_required_extensions(view: FigpackView) -> Set[str]:
110
+ def _discover_required_extensions(view: FigpackView) -> List[str]:
111
111
  """
112
112
  Recursively discover all extensions required by a view and its children
113
113
 
@@ -187,6 +187,6 @@ class FileUploadCORSRequestHandler(CORSRequestHandler):
187
187
  self.send_error(500, f"Internal Server Error: {str(e)}")
188
188
  return False
189
189
 
190
- def log_message(self, fmt, *args):
190
+ def log_message(self, format, *args):
191
191
  """Override to suppress default logging (same as parent class)."""
192
192
  pass
@@ -15,9 +15,9 @@ def _save_figure(
15
15
  view: FigpackView instance to save
16
16
  output_path: Output path (destination folder or .tar.gz file path)
17
17
  """
18
- output_path = pathlib.Path(output_path)
19
- if (output_path.suffix == ".gz" and output_path.suffixes[-2] == ".tar") or (
20
- output_path.suffix == ".tgz"
18
+ output_path_2 = pathlib.Path(output_path)
19
+ if (output_path_2.suffix == ".gz" and output_path_2.suffixes[-2] == ".tar") or (
20
+ output_path_2.suffix == ".tgz"
21
21
  ):
22
22
  # It's a .tar.gz file
23
23
  with tempfile.TemporaryDirectory(prefix="figpack_save_") as tmpdir:
@@ -25,11 +25,11 @@ def _save_figure(
25
25
  # Create tar.gz file
26
26
  import tarfile
27
27
 
28
- with tarfile.open(output_path, "w:gz") as tar:
28
+ with tarfile.open(output_path_2, "w:gz") as tar:
29
29
  tar.add(tmpdir, arcname=".")
30
30
  else:
31
31
  # It's a folder
32
- output_path.mkdir(parents=True, exist_ok=True)
32
+ output_path_2.mkdir(parents=True, exist_ok=True)
33
33
  prepare_figure_bundle(
34
- view, str(output_path), title=title, description=description
34
+ view, str(output_path_2), title=title, description=description
35
35
  )
@@ -45,7 +45,7 @@ class CORSRequestHandler(SimpleHTTPRequestHandler):
45
45
  """Reject PUT requests when file upload is not enabled."""
46
46
  self.send_error(405, "Method Not Allowed")
47
47
 
48
- def log_message(self, fmt, *args):
48
+ def log_message(self, format, *args):
49
49
  pass
50
50
 
51
51
 
@@ -189,6 +189,7 @@ class ProcessServerManager:
189
189
  and self._server_thread.is_alive()
190
190
  and (allow_origin is None or self._allow_origin == allow_origin)
191
191
  ):
192
+ assert self._port is not None
192
193
  return f"http://localhost:{self._port}", self._port
193
194
 
194
195
  # Stop existing server if settings are incompatible
@@ -209,7 +210,7 @@ class ProcessServerManager:
209
210
  if enable_file_upload:
210
211
  from ._file_handler import FileUploadCORSRequestHandler
211
212
 
212
- def handler_factory(*args, **kwargs):
213
+ def handler_factory_enable_upload(*args, **kwargs):
213
214
  return FileUploadCORSRequestHandler(
214
215
  *args,
215
216
  directory=str(temp_dir),
@@ -219,6 +220,11 @@ class ProcessServerManager:
219
220
  **kwargs,
220
221
  )
221
222
 
223
+ assert port is not None
224
+ self._server = ThreadingHTTPServer(
225
+ ("0.0.0.0", port), handler_factory_enable_upload
226
+ )
227
+
222
228
  else:
223
229
 
224
230
  def handler_factory(*args, **kwargs):
@@ -226,7 +232,8 @@ class ProcessServerManager:
226
232
  *args, directory=str(temp_dir), allow_origin=allow_origin, **kwargs
227
233
  )
228
234
 
229
- self._server = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
235
+ assert port is not None
236
+ self._server = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
230
237
  self._port = port
231
238
  self._allow_origin = allow_origin
232
239
 
@@ -19,7 +19,7 @@ def _is_in_notebook() -> bool:
19
19
  """
20
20
  try:
21
21
  # Check if IPython is available and we're in a notebook
22
- from IPython import get_ipython
22
+ from IPython import get_ipython # type: ignore
23
23
 
24
24
  ipython = get_ipython()
25
25
  if ipython is None:
@@ -1,3 +1,4 @@
1
+ from typing import Optional, Union
1
2
  import hashlib
2
3
  import json
3
4
  import pathlib
@@ -114,6 +115,7 @@ def _upload_single_file_with_signed_url(
114
115
  else:
115
116
  break
116
117
 
118
+ assert last_exception is not None
117
119
  raise last_exception
118
120
 
119
121
 
@@ -154,10 +156,10 @@ def _compute_deterministic_figure_hash(tmpdir_path: pathlib.Path) -> str:
154
156
 
155
157
  def _create_or_get_figure(
156
158
  figure_hash: str,
157
- api_key: str,
158
- total_files: int = None,
159
- total_size: int = None,
160
- title: str = None,
159
+ api_key: Optional[str],
160
+ total_files: Optional[int] = None,
161
+ total_size: Optional[int] = None,
162
+ title: Optional[str] = None,
161
163
  ephemeral: bool = False,
162
164
  ) -> dict:
163
165
  """
@@ -178,7 +180,7 @@ def _create_or_get_figure(
178
180
  if not ephemeral and api_key is None:
179
181
  raise ValueError("API key is required for non-ephemeral figures")
180
182
 
181
- payload = {
183
+ payload: dict[str, Union[str, int]] = {
182
184
  "figureHash": figure_hash,
183
185
  "figpackVersion": __version__,
184
186
  "bucket": FIGPACK_BUCKET,
@@ -252,8 +254,8 @@ def _finalize_figure(figure_url: str, api_key: str) -> dict:
252
254
 
253
255
  def _upload_bundle(
254
256
  tmpdir: str,
255
- api_key: str,
256
- title: str = None,
257
+ api_key: Optional[str],
258
+ title: Optional[str] = None,
257
259
  ephemeral: bool = False,
258
260
  use_consolidated_metadata_only: bool = False,
259
261
  ) -> str:
@@ -329,7 +331,9 @@ def _upload_bundle(
329
331
 
330
332
  # Get signed URLs for this batch
331
333
  try:
332
- signed_urls_map = _get_batch_signed_urls(figure_url, batch, api_key)
334
+ signed_urls_map = _get_batch_signed_urls(
335
+ figure_url, batch, api_key if api_key else ""
336
+ )
333
337
  except Exception as e:
334
338
  print(f"Failed to get signed URLs for batch {batch_num}: {e}")
335
339
  raise
@@ -400,7 +404,9 @@ def _upload_bundle(
400
404
  try:
401
405
  # Use batch API for manifest
402
406
  manifest_batch = [("manifest.json", temp_file_path)]
403
- signed_urls_map = _get_batch_signed_urls(figure_url, manifest_batch, api_key)
407
+ signed_urls_map = _get_batch_signed_urls(
408
+ figure_url, manifest_batch, api_key if api_key else ""
409
+ )
404
410
 
405
411
  if "manifest.json" not in signed_urls_map:
406
412
  raise Exception("No signed URL returned for manifest.json")
@@ -418,7 +424,7 @@ def _upload_bundle(
418
424
 
419
425
  # Finalize the figure upload
420
426
  print("Finalizing figure...")
421
- _finalize_figure(figure_url, api_key)
427
+ _finalize_figure(figure_url, api_key if api_key else "")
422
428
  print("Upload completed successfully")
423
429
 
424
430
  return figure_url
@@ -34,10 +34,10 @@ def serve_files(
34
34
  enable_file_upload: Whether to enable PUT requests for file uploads
35
35
  max_file_size: Maximum file size in bytes for uploads (default 10MB)
36
36
  """
37
- tmpdir = pathlib.Path(tmpdir)
38
- tmpdir = tmpdir.resolve()
39
- if not tmpdir.exists() or not tmpdir.is_dir():
40
- raise SystemExit(f"Directory not found: {tmpdir}")
37
+ tmpdir_2 = pathlib.Path(tmpdir)
38
+ tmpdir_2 = tmpdir_2.resolve()
39
+ if not tmpdir_2.exists() or not tmpdir_2.is_dir():
40
+ raise SystemExit(f"Directory not found: {tmpdir_2}")
41
41
 
42
42
  # Create a temporary server manager instance for this specific directory
43
43
  # Note: We can't use the singleton ProcessServerManager here because it serves
@@ -56,29 +56,34 @@ def serve_files(
56
56
  # Choose handler based on file upload requirement
57
57
  if enable_file_upload:
58
58
 
59
- def handler_factory(*args, **kwargs):
59
+ def handler_factory_upload_enabled(*args, **kwargs):
60
60
  return FileUploadCORSRequestHandler(
61
61
  *args,
62
- directory=str(tmpdir),
62
+ directory=str(tmpdir_2),
63
63
  allow_origin=allow_origin,
64
64
  enable_file_upload=True,
65
65
  max_file_size=max_file_size,
66
66
  **kwargs,
67
67
  )
68
68
 
69
- upload_status = " (file upload enabled)" if enable_file_upload else ""
69
+ upload_status = (
70
+ " (file upload enabled)" if handler_factory_upload_enabled else ""
71
+ )
72
+
73
+ httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory_upload_enabled) # type: ignore
70
74
  else:
71
75
 
72
76
  def handler_factory(*args, **kwargs):
73
77
  return CORSRequestHandler(
74
- *args, directory=str(tmpdir), allow_origin=allow_origin, **kwargs
78
+ *args, directory=str(tmpdir_2), allow_origin=allow_origin, **kwargs
75
79
  )
76
80
 
77
81
  upload_status = ""
78
82
 
79
- httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory)
83
+ httpd = ThreadingHTTPServer(("0.0.0.0", port), handler_factory) # type: ignore
84
+
80
85
  print(
81
- f"Serving {tmpdir} at http://localhost:{port} (CORS → {allow_origin}){upload_status}"
86
+ f"Serving {tmpdir_2} at http://localhost:{port} (CORS → {allow_origin}){upload_status}"
82
87
  )
83
88
  thread = threading.Thread(target=httpd.serve_forever, daemon=True)
84
89
  thread.start()
@@ -2,9 +2,13 @@
2
2
  Base class for views that use figpack extensions
3
3
  """
4
4
 
5
+ from typing import TYPE_CHECKING
6
+
5
7
  from .figpack_view import FigpackView
6
- from .figpack_extension import FigpackExtension
7
- from ..core.zarr import Group
8
+
9
+ if TYPE_CHECKING:
10
+ from .figpack_extension import FigpackExtension
11
+ from .zarr import Group
8
12
 
9
13
 
10
14
  class ExtensionView(FigpackView):
@@ -12,7 +16,7 @@ class ExtensionView(FigpackView):
12
16
  Base class for views that are rendered by figpack extensions
13
17
  """
14
18
 
15
- def __init__(self, *, extension: FigpackExtension, view_type: str):
19
+ def __init__(self, *, extension: "FigpackExtension", view_type: str) -> None:
16
20
  """
17
21
  Initialize an extension-based view
18
22
 
@@ -23,7 +27,7 @@ class ExtensionView(FigpackView):
23
27
  self.extension = extension
24
28
  self.view_type = view_type
25
29
 
26
- def write_to_zarr_group(self, group: Group) -> None:
30
+ def write_to_zarr_group(self, group: "Group") -> None:
27
31
  """
28
32
  Write the extension view metadata to a Zarr group.
29
33
  Subclasses should call super().write_to_zarr_group(group) first,
@@ -17,7 +17,7 @@ class FigpackExtension:
17
17
  javascript_code: str,
18
18
  additional_files: Optional[Dict[str, str]] = None,
19
19
  version: str = "1.0.0",
20
- ):
20
+ ) -> None:
21
21
  """
22
22
  Initialize a figpack extension
23
23
 
@@ -5,7 +5,7 @@ Base view class for figpack visualization components
5
5
  import os
6
6
  import random
7
7
  import string
8
- from typing import Union
8
+ from typing import Optional
9
9
 
10
10
  from .zarr import Group
11
11
 
@@ -19,17 +19,17 @@ class FigpackView:
19
19
  self,
20
20
  *,
21
21
  title: str,
22
- description: Union[str, None] = None,
23
- port: Union[int, None] = None,
24
- open_in_browser: Union[bool, None] = None,
25
- upload: Union[bool, None] = None,
26
- inline: Union[bool, None] = None,
22
+ description: Optional[str] = None,
23
+ port: Optional[int] = None,
24
+ open_in_browser: Optional[bool] = None,
25
+ upload: Optional[bool] = None,
26
+ inline: Optional[bool] = None,
27
27
  inline_height: int = 600,
28
- ephemeral: Union[bool, None] = None,
29
- allow_origin: Union[str, None] = None,
30
- wait_for_input: Union[bool, None] = None,
31
- _dev: Union[bool, None] = None,
32
- ):
28
+ ephemeral: Optional[bool] = None,
29
+ allow_origin: Optional[str] = None,
30
+ wait_for_input: Optional[bool] = None,
31
+ _dev: Optional[bool] = None,
32
+ ) -> None:
33
33
  """
34
34
  Display a figpack view component with intelligent environment detection and flexible display options.
35
35
  See https://flatironinstitute.github.io/figpack/show_function.html for complete documentation.
@@ -86,6 +86,8 @@ class FigpackView:
86
86
  inline = False
87
87
  elif _is_in_notebook() and not upload:
88
88
  inline = True
89
+ else:
90
+ inline = False
89
91
 
90
92
  # determine open_in_browser
91
93
  if open_in_browser is None:
@@ -111,13 +113,19 @@ class FigpackView:
111
113
  upload = True
112
114
  ephemeral = True
113
115
 
116
+ if ephemeral is None:
117
+ ephemeral = False
118
+
119
+ if upload is None:
120
+ upload = False
121
+
114
122
  # determine _dev
115
123
  if _dev is None:
116
124
  _dev = os.environ.get("FIGPACK_DEV") == "1"
117
125
 
118
126
  if port is None and os.environ.get("FIGPACK_PORT"):
119
127
  try:
120
- port = int(os.environ.get("FIGPACK_PORT"))
128
+ port = int(os.environ.get("FIGPACK_PORT", ""))
121
129
  except Exception:
122
130
  pass
123
131
 
@@ -129,6 +137,8 @@ class FigpackView:
129
137
  if ephemeral and not upload:
130
138
  raise ValueError("ephemeral=True requires upload=True to be set")
131
139
 
140
+ _local_figure_name: Optional[str] = None
141
+
132
142
  if _dev:
133
143
  if open_in_browser:
134
144
  print("** Note: In dev mode, open_in_browser is forced to False **")
figpack/core/zarr.py CHANGED
@@ -33,13 +33,13 @@ class Group:
33
33
  if _check_zarr_version() == 2:
34
34
  self._zarr_group.create_dataset(name, **kwargs)
35
35
  elif _check_zarr_version() == 3:
36
- self._zarr_group.create_array(name, **kwargs)
36
+ self._zarr_group.create_array(name, **kwargs) # type: ignore
37
37
  else:
38
38
  raise RuntimeError("Unsupported Zarr version")
39
39
 
40
40
  @property
41
41
  def attrs(self) -> Dict[str, Any]:
42
- return self._zarr_group.attrs
42
+ return self._zarr_group.attrs # type: ignore
43
43
 
44
44
  def __getitem__(self, key: str) -> Any:
45
45
  return self._zarr_group[key]