figpack 0.2.17__py3-none-any.whl → 0.2.40__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.
- figpack/__init__.py +1 -1
- figpack/cli.py +288 -2
- figpack/core/_bundle_utils.py +40 -7
- figpack/core/_file_handler.py +195 -0
- figpack/core/_save_figure.py +12 -8
- figpack/core/_server_manager.py +146 -7
- figpack/core/_show_view.py +2 -2
- figpack/core/_upload_bundle.py +63 -53
- figpack/core/_view_figure.py +48 -12
- figpack/core/_zarr_consolidate.py +185 -0
- figpack/core/extension_view.py +9 -5
- figpack/core/figpack_extension.py +1 -1
- figpack/core/figpack_view.py +52 -21
- figpack/core/zarr.py +2 -2
- figpack/extensions.py +356 -0
- figpack/figpack-figure-dist/assets/index-ST_DU17U.js +95 -0
- figpack/figpack-figure-dist/assets/index-V5m_wCvw.css +1 -0
- figpack/figpack-figure-dist/index.html +6 -2
- figpack/views/Box.py +4 -4
- figpack/views/CaptionedView.py +64 -0
- figpack/views/DataFrame.py +1 -1
- figpack/views/Gallery.py +2 -2
- figpack/views/Iframe.py +43 -0
- figpack/views/Image.py +2 -3
- figpack/views/Markdown.py +8 -4
- figpack/views/MatplotlibFigure.py +1 -1
- figpack/views/MountainLayout.py +72 -0
- figpack/views/MountainLayoutItem.py +50 -0
- figpack/views/MultiChannelTimeseries.py +1 -1
- figpack/views/PlotlyExtension/PlotlyExtension.py +14 -14
- figpack/views/Spectrogram.py +3 -1
- figpack/views/Splitter.py +3 -3
- figpack/views/TabLayout.py +2 -2
- figpack/views/TimeseriesGraph.py +113 -20
- figpack/views/__init__.py +4 -0
- {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/METADATA +25 -1
- figpack-0.2.40.dist-info/RECORD +50 -0
- figpack/figpack-figure-dist/assets/index-DBwmtEpB.js +0 -91
- figpack/figpack-figure-dist/assets/index-DHWczh-Q.css +0 -1
- figpack-0.2.17.dist-info/RECORD +0 -43
- {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/WHEEL +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/entry_points.txt +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/licenses/LICENSE +0 -0
- {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def consolidate_zarr_chunks(
|
|
8
|
+
zarr_dir: pathlib.Path, max_file_size: int = 100_000_000
|
|
9
|
+
) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Consolidate zarr chunk files into larger files to reduce the number of files
|
|
12
|
+
that need to be uploaded. Updates the .zmetadata file with refs mapping.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
zarr_dir: Path to the zarr directory
|
|
16
|
+
max_file_size: Maximum size for each consolidated file in bytes (default: 100 MB)
|
|
17
|
+
"""
|
|
18
|
+
if not zarr_dir.is_dir():
|
|
19
|
+
raise ValueError(f"Expected a directory, got: {zarr_dir}")
|
|
20
|
+
|
|
21
|
+
# Read the existing .zmetadata file
|
|
22
|
+
zmetadata_path = zarr_dir / ".zmetadata"
|
|
23
|
+
if not zmetadata_path.exists():
|
|
24
|
+
raise ValueError(f"No .zmetadata file found at {zmetadata_path}")
|
|
25
|
+
|
|
26
|
+
with open(zmetadata_path, "r") as f:
|
|
27
|
+
zmetadata = json.load(f)
|
|
28
|
+
|
|
29
|
+
# Collect all chunk files (non-metadata files)
|
|
30
|
+
chunk_files = _collect_chunk_files(zarr_dir)
|
|
31
|
+
|
|
32
|
+
if not chunk_files:
|
|
33
|
+
# No chunk files to consolidate
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Group chunk files into consolidated files
|
|
37
|
+
consolidated_groups = _group_files_by_size(chunk_files, max_file_size)
|
|
38
|
+
|
|
39
|
+
# Create consolidated files and build refs mapping
|
|
40
|
+
refs: Dict[str, List] = {}
|
|
41
|
+
for group_idx, file_group in enumerate(consolidated_groups):
|
|
42
|
+
consolidated_filename = f"_consolidated_{group_idx}.dat"
|
|
43
|
+
consolidated_path = zarr_dir / consolidated_filename
|
|
44
|
+
|
|
45
|
+
# Write the consolidated file and track byte offsets
|
|
46
|
+
current_offset = 0
|
|
47
|
+
with open(consolidated_path, "wb") as consolidated_file:
|
|
48
|
+
for file_path, relative_path in file_group:
|
|
49
|
+
# Read the chunk file
|
|
50
|
+
with open(file_path, "rb") as chunk_file:
|
|
51
|
+
chunk_data = chunk_file.read()
|
|
52
|
+
|
|
53
|
+
# Write to consolidated file
|
|
54
|
+
consolidated_file.write(chunk_data)
|
|
55
|
+
|
|
56
|
+
# Add to refs mapping
|
|
57
|
+
refs[relative_path] = [
|
|
58
|
+
consolidated_filename,
|
|
59
|
+
current_offset,
|
|
60
|
+
len(chunk_data),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# Update offset
|
|
64
|
+
current_offset += len(chunk_data)
|
|
65
|
+
|
|
66
|
+
# Update .zmetadata with refs
|
|
67
|
+
zmetadata["refs"] = refs
|
|
68
|
+
|
|
69
|
+
# Write updated .zmetadata
|
|
70
|
+
with open(zmetadata_path, "w") as f:
|
|
71
|
+
json.dump(zmetadata, f, indent=2)
|
|
72
|
+
|
|
73
|
+
# Delete original chunk files
|
|
74
|
+
for file_path, _ in chunk_files:
|
|
75
|
+
try:
|
|
76
|
+
file_path.unlink()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Warning: could not remove file {file_path}: {e}")
|
|
79
|
+
|
|
80
|
+
# Clean up empty directories
|
|
81
|
+
_remove_empty_directories(zarr_dir)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _collect_chunk_files(zarr_dir: pathlib.Path) -> List[Tuple[pathlib.Path, str]]:
|
|
85
|
+
"""
|
|
86
|
+
Collect all chunk files in the zarr directory (excluding metadata files).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
zarr_dir: Path to the zarr directory
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of tuples (absolute_path, relative_path) for each chunk file
|
|
93
|
+
"""
|
|
94
|
+
chunk_files = []
|
|
95
|
+
metadata_files = {".zmetadata", ".zarray", ".zgroup", ".zattrs"}
|
|
96
|
+
|
|
97
|
+
for root, dirs, files in os.walk(zarr_dir):
|
|
98
|
+
for file in files:
|
|
99
|
+
# Skip metadata files
|
|
100
|
+
if file in metadata_files or file.startswith("_consolidated_"):
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
file_path = pathlib.Path(root) / file
|
|
104
|
+
# Get relative path from zarr_dir
|
|
105
|
+
relative_path = file_path.relative_to(zarr_dir).as_posix()
|
|
106
|
+
|
|
107
|
+
chunk_files.append((file_path, relative_path))
|
|
108
|
+
|
|
109
|
+
return chunk_files
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _group_files_by_size(
|
|
113
|
+
files: List[Tuple[pathlib.Path, str]], max_size: int
|
|
114
|
+
) -> List[List[Tuple[pathlib.Path, str]]]:
|
|
115
|
+
"""
|
|
116
|
+
Group files into bins where each bin's total size is <= max_size.
|
|
117
|
+
|
|
118
|
+
Uses a simple first-fit bin packing algorithm.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
files: List of (file_path, relative_path) tuples
|
|
122
|
+
max_size: Maximum total size for each group in bytes
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of groups, where each group is a list of (file_path, relative_path) tuples
|
|
126
|
+
"""
|
|
127
|
+
# Get file sizes
|
|
128
|
+
files_with_sizes = []
|
|
129
|
+
for file_path, relative_path in files:
|
|
130
|
+
try:
|
|
131
|
+
size = file_path.stat().st_size
|
|
132
|
+
files_with_sizes.append((file_path, relative_path, size))
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"Warning: could not get size of {file_path}: {e}")
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Sort by size (largest first) for better packing
|
|
138
|
+
files_with_sizes.sort(key=lambda x: x[2], reverse=True)
|
|
139
|
+
|
|
140
|
+
# First-fit bin packing
|
|
141
|
+
groups: List[List[Tuple[pathlib.Path, str]]] = []
|
|
142
|
+
group_sizes: List[int] = []
|
|
143
|
+
|
|
144
|
+
for file_path, relative_path, size in files_with_sizes:
|
|
145
|
+
# If file is larger than max_size, put it in its own group
|
|
146
|
+
if size > max_size:
|
|
147
|
+
groups.append([(file_path, relative_path)])
|
|
148
|
+
group_sizes.append(size)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# Try to fit into existing group
|
|
152
|
+
placed = False
|
|
153
|
+
for i, group_size in enumerate(group_sizes):
|
|
154
|
+
if group_size + size <= max_size:
|
|
155
|
+
groups[i].append((file_path, relative_path))
|
|
156
|
+
group_sizes[i] += size
|
|
157
|
+
placed = True
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
# If doesn't fit anywhere, create new group
|
|
161
|
+
if not placed:
|
|
162
|
+
groups.append([(file_path, relative_path)])
|
|
163
|
+
group_sizes.append(size)
|
|
164
|
+
|
|
165
|
+
return groups
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _remove_empty_directories(zarr_dir: pathlib.Path) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Remove empty directories within the zarr directory.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
zarr_dir: Path to the zarr directory
|
|
174
|
+
"""
|
|
175
|
+
# Walk bottom-up so we can remove empty parent directories
|
|
176
|
+
for root, dirs, files in os.walk(zarr_dir, topdown=False):
|
|
177
|
+
for dir_name in dirs:
|
|
178
|
+
dir_path = pathlib.Path(root) / dir_name
|
|
179
|
+
try:
|
|
180
|
+
# Only remove if directory is empty
|
|
181
|
+
if not any(dir_path.iterdir()):
|
|
182
|
+
dir_path.rmdir()
|
|
183
|
+
except Exception:
|
|
184
|
+
# Directory not empty or other error, skip
|
|
185
|
+
pass
|
figpack/core/extension_view.py
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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,10 +27,10 @@ class ExtensionView(FigpackView):
|
|
|
23
27
|
self.extension = extension
|
|
24
28
|
self.view_type = view_type
|
|
25
29
|
|
|
26
|
-
def
|
|
30
|
+
def write_to_zarr_group(self, group: "Group") -> None:
|
|
27
31
|
"""
|
|
28
32
|
Write the extension view metadata to a Zarr group.
|
|
29
|
-
Subclasses should call super().
|
|
33
|
+
Subclasses should call super().write_to_zarr_group(group) first,
|
|
30
34
|
then add their own data.
|
|
31
35
|
|
|
32
36
|
Args:
|
figpack/core/figpack_view.py
CHANGED
|
@@ -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
|
|
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:
|
|
23
|
-
port:
|
|
24
|
-
open_in_browser:
|
|
25
|
-
upload:
|
|
26
|
-
inline:
|
|
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:
|
|
29
|
-
allow_origin:
|
|
30
|
-
wait_for_input:
|
|
31
|
-
_dev:
|
|
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.
|
|
@@ -60,13 +60,23 @@ class FigpackView:
|
|
|
60
60
|
|
|
61
61
|
# determine upload
|
|
62
62
|
if upload is None:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if os.environ.get("FIGPACK_UPLOAD") == "1":
|
|
64
|
+
upload = True
|
|
65
|
+
elif os.environ.get("FIGPACK_UPLOAD") == "0":
|
|
66
|
+
upload = False
|
|
67
|
+
|
|
68
|
+
if upload is True:
|
|
69
|
+
if ephemeral is True:
|
|
66
70
|
# ephemeral is reserved for the case where we don't specify upload
|
|
67
71
|
# and we are in a notebook in a remote environment such as
|
|
68
72
|
# colab or jupyterhub
|
|
69
|
-
raise ValueError(
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"ephemeral cannot be set to True if upload is set to True"
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
ephemeral = (
|
|
78
|
+
False # if we excplicitly set upload=True, force ephemeral=False
|
|
79
|
+
)
|
|
70
80
|
|
|
71
81
|
# determine inline
|
|
72
82
|
if inline is None:
|
|
@@ -76,15 +86,22 @@ class FigpackView:
|
|
|
76
86
|
inline = False
|
|
77
87
|
elif _is_in_notebook() and not upload:
|
|
78
88
|
inline = True
|
|
89
|
+
else:
|
|
90
|
+
inline = False
|
|
79
91
|
|
|
80
92
|
# determine open_in_browser
|
|
81
93
|
if open_in_browser is None:
|
|
82
94
|
open_in_browser = os.environ.get("FIGPACK_OPEN_IN_BROWSER") == "1"
|
|
83
95
|
|
|
84
96
|
# determine ephemeral
|
|
85
|
-
if ephemeral is None:
|
|
97
|
+
if ephemeral is None and not upload:
|
|
86
98
|
ephemeral = False # default to False
|
|
87
|
-
if
|
|
99
|
+
if os.environ.get("FIGPACK_REMOTE_ENV") == "1":
|
|
100
|
+
ephemeral = True
|
|
101
|
+
upload = True
|
|
102
|
+
elif os.environ.get("FIGPACK_REMOTE_ENV") == "0":
|
|
103
|
+
ephemeral = False
|
|
104
|
+
elif _is_in_notebook():
|
|
88
105
|
if _is_in_colab():
|
|
89
106
|
# if we are in a notebook and in colab, we should show as uploaded ephemeral
|
|
90
107
|
print("Detected Google Colab notebook environment.")
|
|
@@ -96,10 +113,22 @@ class FigpackView:
|
|
|
96
113
|
upload = True
|
|
97
114
|
ephemeral = True
|
|
98
115
|
|
|
116
|
+
if ephemeral is None:
|
|
117
|
+
ephemeral = False
|
|
118
|
+
|
|
119
|
+
if upload is None:
|
|
120
|
+
upload = False
|
|
121
|
+
|
|
99
122
|
# determine _dev
|
|
100
123
|
if _dev is None:
|
|
101
124
|
_dev = os.environ.get("FIGPACK_DEV") == "1"
|
|
102
125
|
|
|
126
|
+
if port is None and os.environ.get("FIGPACK_PORT"):
|
|
127
|
+
try:
|
|
128
|
+
port = int(os.environ.get("FIGPACK_PORT", ""))
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
103
132
|
# determine wait_for_input
|
|
104
133
|
if wait_for_input is None:
|
|
105
134
|
wait_for_input = not _is_in_notebook()
|
|
@@ -108,6 +137,8 @@ class FigpackView:
|
|
|
108
137
|
if ephemeral and not upload:
|
|
109
138
|
raise ValueError("ephemeral=True requires upload=True to be set")
|
|
110
139
|
|
|
140
|
+
_local_figure_name: Optional[str] = None
|
|
141
|
+
|
|
111
142
|
if _dev:
|
|
112
143
|
if open_in_browser:
|
|
113
144
|
print("** Note: In dev mode, open_in_browser is forced to False **")
|
|
@@ -145,7 +176,7 @@ class FigpackView:
|
|
|
145
176
|
_local_figure_name=_local_figure_name if _dev else None,
|
|
146
177
|
)
|
|
147
178
|
|
|
148
|
-
def save(self, output_path: str, *, title: str) -> None:
|
|
179
|
+
def save(self, output_path: str, *, title: str, description: str = "") -> None:
|
|
149
180
|
"""
|
|
150
181
|
Save as figure either to a folder or to a .tar.gz file
|
|
151
182
|
Args:
|
|
@@ -153,13 +184,13 @@ class FigpackView:
|
|
|
153
184
|
"""
|
|
154
185
|
from ._save_figure import _save_figure
|
|
155
186
|
|
|
156
|
-
_save_figure(self, output_path, title=title)
|
|
187
|
+
_save_figure(self, output_path, title=title, description=description)
|
|
157
188
|
|
|
158
|
-
def
|
|
189
|
+
def write_to_zarr_group(self, group: Group) -> None:
|
|
159
190
|
"""
|
|
160
191
|
Write the view data to a Zarr group. Must be implemented by subclasses.
|
|
161
192
|
|
|
162
193
|
Args:
|
|
163
194
|
group: Zarr group to write data into
|
|
164
195
|
"""
|
|
165
|
-
raise NotImplementedError("Subclasses must implement
|
|
196
|
+
raise NotImplementedError("Subclasses must implement write_to_zarr_group")
|
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]
|