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.
Files changed (44) hide show
  1. figpack/__init__.py +1 -1
  2. figpack/cli.py +288 -2
  3. figpack/core/_bundle_utils.py +40 -7
  4. figpack/core/_file_handler.py +195 -0
  5. figpack/core/_save_figure.py +12 -8
  6. figpack/core/_server_manager.py +146 -7
  7. figpack/core/_show_view.py +2 -2
  8. figpack/core/_upload_bundle.py +63 -53
  9. figpack/core/_view_figure.py +48 -12
  10. figpack/core/_zarr_consolidate.py +185 -0
  11. figpack/core/extension_view.py +9 -5
  12. figpack/core/figpack_extension.py +1 -1
  13. figpack/core/figpack_view.py +52 -21
  14. figpack/core/zarr.py +2 -2
  15. figpack/extensions.py +356 -0
  16. figpack/figpack-figure-dist/assets/index-ST_DU17U.js +95 -0
  17. figpack/figpack-figure-dist/assets/index-V5m_wCvw.css +1 -0
  18. figpack/figpack-figure-dist/index.html +6 -2
  19. figpack/views/Box.py +4 -4
  20. figpack/views/CaptionedView.py +64 -0
  21. figpack/views/DataFrame.py +1 -1
  22. figpack/views/Gallery.py +2 -2
  23. figpack/views/Iframe.py +43 -0
  24. figpack/views/Image.py +2 -3
  25. figpack/views/Markdown.py +8 -4
  26. figpack/views/MatplotlibFigure.py +1 -1
  27. figpack/views/MountainLayout.py +72 -0
  28. figpack/views/MountainLayoutItem.py +50 -0
  29. figpack/views/MultiChannelTimeseries.py +1 -1
  30. figpack/views/PlotlyExtension/PlotlyExtension.py +14 -14
  31. figpack/views/Spectrogram.py +3 -1
  32. figpack/views/Splitter.py +3 -3
  33. figpack/views/TabLayout.py +2 -2
  34. figpack/views/TimeseriesGraph.py +113 -20
  35. figpack/views/__init__.py +4 -0
  36. {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/METADATA +25 -1
  37. figpack-0.2.40.dist-info/RECORD +50 -0
  38. figpack/figpack-figure-dist/assets/index-DBwmtEpB.js +0 -91
  39. figpack/figpack-figure-dist/assets/index-DHWczh-Q.css +0 -1
  40. figpack-0.2.17.dist-info/RECORD +0 -43
  41. {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/WHEEL +0 -0
  42. {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/entry_points.txt +0 -0
  43. {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/licenses/LICENSE +0 -0
  44. {figpack-0.2.17.dist-info → figpack-0.2.40.dist-info}/top_level.txt +0 -0
figpack/extensions.py ADDED
@@ -0,0 +1,356 @@
1
+ """
2
+ Extension management functionality for figpack
3
+ """
4
+
5
+ import json
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from typing import Dict, List, Optional, Tuple
10
+ from urllib.parse import urljoin
11
+
12
+ import requests
13
+
14
+
15
+ # Default wheel repository URL
16
+ DEFAULT_WHEEL_REPO_URL = "https://flatironinstitute.github.io/figpack/wheels/"
17
+
18
+
19
+ def parse_wheel_filename(filename: str) -> Optional[Dict[str, str]]:
20
+ """
21
+ Parse a wheel filename to extract package information.
22
+
23
+ Wheel filename format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
24
+ """
25
+ if not filename.endswith(".whl"):
26
+ return None
27
+
28
+ # Remove .whl extension
29
+ basename = filename[:-4]
30
+
31
+ # Split on hyphens, but be careful about package names with hyphens
32
+ parts = basename.split("-")
33
+
34
+ if len(parts) < 5:
35
+ return None
36
+
37
+ # The last 3 parts are always python_tag, abi_tag, platform_tag
38
+ python_tag = parts[-3]
39
+ abi_tag = parts[-2]
40
+ platform_tag = parts[-1]
41
+
42
+ # Everything before the last 3 parts, split between name and version
43
+ name_version_parts = parts[:-3]
44
+
45
+ # Find where version starts (first part that looks like a version number)
46
+ version_start_idx = 1 # Default to second part
47
+ for i, part in enumerate(name_version_parts[1:], 1):
48
+ if re.match(r"^\d+", part): # Starts with a digit
49
+ version_start_idx = i
50
+ break
51
+
52
+ name = "-".join(name_version_parts[:version_start_idx])
53
+ version = "-".join(name_version_parts[version_start_idx:])
54
+
55
+ return {
56
+ "name": name,
57
+ "version": version,
58
+ "python_tag": python_tag,
59
+ "abi_tag": abi_tag,
60
+ "platform_tag": platform_tag,
61
+ }
62
+
63
+
64
+ class ExtensionManager:
65
+ """Manages figpack extension packages"""
66
+
67
+ def __init__(self, wheel_repo_url: str = DEFAULT_WHEEL_REPO_URL):
68
+ self.wheel_repo_url = wheel_repo_url.rstrip("/") + "/"
69
+ self._available_cache = None
70
+
71
+ def get_installed_packages(self) -> Dict[str, str]:
72
+ """Get currently installed figpack extension packages and their versions"""
73
+ try:
74
+ result = subprocess.run(
75
+ [sys.executable, "-m", "pip", "list", "--format=json"],
76
+ capture_output=True,
77
+ text=True,
78
+ check=True,
79
+ )
80
+ installed_packages = json.loads(result.stdout)
81
+
82
+ # Get available extensions to filter against
83
+ available_extensions = self.get_available_packages()
84
+
85
+ # Filter for figpack extensions
86
+ figpack_extensions = {}
87
+ for pkg in installed_packages:
88
+ pkg_name = pkg["name"]
89
+ if pkg_name in available_extensions:
90
+ figpack_extensions[pkg_name] = pkg["version"]
91
+
92
+ return figpack_extensions
93
+ except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
94
+ print(f"Warning: Could not get installed packages: {e}")
95
+ return {}
96
+
97
+ def get_available_packages(self) -> Dict[str, Dict[str, str]]:
98
+ """
99
+ Get available packages from the wheel repository.
100
+
101
+ Returns:
102
+ Dict mapping package names to their info (version, description, etc.)
103
+ """
104
+ if self._available_cache is not None:
105
+ return self._available_cache
106
+
107
+ try:
108
+ # Try the simple index first as it's easier to parse
109
+ packages = self._get_packages_from_simple_index()
110
+
111
+ # If that fails, try the main index.html
112
+ if not packages:
113
+ packages = self._get_packages_from_main_index()
114
+
115
+ self._available_cache = packages
116
+ return packages
117
+
118
+ except requests.RequestException as e:
119
+ print(f"Warning: Could not fetch available packages: {e}")
120
+ return {}
121
+
122
+ def _get_packages_from_simple_index(self) -> Dict[str, Dict[str, str]]:
123
+ """Get packages from simple.html using regex parsing"""
124
+ try:
125
+ simple_url = urljoin(self.wheel_repo_url, "simple.html")
126
+ response = requests.get(simple_url, timeout=10)
127
+ response.raise_for_status()
128
+
129
+ packages = {}
130
+
131
+ # Parse wheel filenames from simple index using regex
132
+ # Look for href attributes containing .whl files
133
+ wheel_pattern = re.compile(r'href="([^"]*\.whl)"')
134
+ matches = wheel_pattern.findall(response.text)
135
+
136
+ for wheel_filename in matches:
137
+ wheel_info = parse_wheel_filename(wheel_filename)
138
+ if wheel_info and wheel_info["name"].startswith("figpack_"):
139
+ pkg_name = wheel_info["name"]
140
+
141
+ if pkg_name not in packages:
142
+ packages[pkg_name] = {
143
+ "version": wheel_info["version"],
144
+ "description": self._get_package_description(pkg_name),
145
+ }
146
+ else:
147
+ current_version = packages[pkg_name]["version"]
148
+ new_version = wheel_info["version"]
149
+ if self._is_newer_version(new_version, current_version):
150
+ packages[pkg_name]["version"] = new_version
151
+
152
+ return packages
153
+
154
+ except requests.RequestException:
155
+ return {}
156
+
157
+ def _get_packages_from_main_index(self) -> Dict[str, Dict[str, str]]:
158
+ """Get packages from main index.html using regex parsing"""
159
+ try:
160
+ index_url = urljoin(self.wheel_repo_url, "index.html")
161
+ response = requests.get(index_url, timeout=10)
162
+ response.raise_for_status()
163
+
164
+ packages = {}
165
+
166
+ # Parse wheel filenames from main index using regex
167
+ # Look for links to .whl files
168
+ wheel_pattern = re.compile(r'<a[^>]+href="([^"]*\.whl)"[^>]*>')
169
+ matches = wheel_pattern.findall(response.text)
170
+
171
+ for wheel_filename in matches:
172
+ wheel_info = parse_wheel_filename(wheel_filename)
173
+ if wheel_info and wheel_info["name"].startswith("figpack_"):
174
+ pkg_name = wheel_info["name"]
175
+
176
+ if pkg_name not in packages:
177
+ packages[pkg_name] = {
178
+ "version": wheel_info["version"],
179
+ "description": self._get_package_description(pkg_name),
180
+ }
181
+ else:
182
+ current_version = packages[pkg_name]["version"]
183
+ new_version = wheel_info["version"]
184
+ if self._is_newer_version(new_version, current_version):
185
+ packages[pkg_name]["version"] = new_version
186
+
187
+ return packages
188
+
189
+ except requests.RequestException:
190
+ return {}
191
+
192
+ def _get_package_description(self, package_name: str) -> str:
193
+ """Get a description for a package based on its name"""
194
+ descriptions = {
195
+ "figpack_3d": "3D visualization extension using Three.js",
196
+ "figpack_force_graph": "Force-directed graph visualization extension",
197
+ "figpack_franklab": "Frank Lab specific neuroscience visualization tools",
198
+ "figpack_spike_sorting": "Spike sorting specific visualization tools",
199
+ }
200
+ return descriptions.get(package_name, "Figpack extension package")
201
+
202
+ def _is_newer_version(self, version1: str, version2: str) -> bool:
203
+ """Simple version comparison - returns True if version1 > version2"""
204
+ try:
205
+ v1_parts = [int(x) for x in version1.split(".")]
206
+ v2_parts = [int(x) for x in version2.split(".")]
207
+
208
+ # Pad shorter version with zeros
209
+ max_len = max(len(v1_parts), len(v2_parts))
210
+ v1_parts.extend([0] * (max_len - len(v1_parts)))
211
+ v2_parts.extend([0] * (max_len - len(v2_parts)))
212
+
213
+ return v1_parts > v2_parts
214
+ except ValueError:
215
+ # If we can't parse versions, just do string comparison
216
+ return version1 > version2
217
+
218
+ def list_extensions(self) -> None:
219
+ """List all available extensions with their status"""
220
+ print("Available figpack extensions:")
221
+ print()
222
+
223
+ installed = self.get_installed_packages()
224
+ available = self.get_available_packages()
225
+
226
+ if not available:
227
+ print("No extensions found in the wheel repository.")
228
+ return
229
+
230
+ for ext_name, ext_info in available.items():
231
+ if ext_name in installed:
232
+ status = f"✓ {ext_name} (installed: {installed[ext_name]}, latest: {ext_info['version']})"
233
+ else:
234
+ status = f"✗ {ext_name} (not installed, latest: {ext_info['version']})"
235
+
236
+ print(f"{status} - {ext_info['description']}")
237
+
238
+ def install_extensions(
239
+ self, extensions: List[str], upgrade: bool = False, install_all: bool = False
240
+ ) -> bool:
241
+ """Install or upgrade extensions"""
242
+ available = self.get_available_packages()
243
+
244
+ if install_all:
245
+ extensions = list(available.keys())
246
+
247
+ if not extensions:
248
+ print("No extensions specified")
249
+ return False
250
+
251
+ # Validate extension names
252
+ invalid_extensions = [ext for ext in extensions if ext not in available]
253
+ if invalid_extensions:
254
+ print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
255
+ print(f"Available extensions: {', '.join(available.keys())}")
256
+ return False
257
+
258
+ success = True
259
+ for extension in extensions:
260
+ if not self._install_single_extension(extension, upgrade):
261
+ success = False
262
+
263
+ return success
264
+
265
+ def _install_single_extension(self, extension: str, upgrade: bool = False) -> bool:
266
+ """Install a single extension"""
267
+ cmd = [
268
+ sys.executable,
269
+ "-m",
270
+ "pip",
271
+ "install",
272
+ "--find-links",
273
+ self.wheel_repo_url,
274
+ extension,
275
+ ]
276
+
277
+ if upgrade:
278
+ cmd.append("--upgrade")
279
+
280
+ try:
281
+ print(f"Installing {extension}...")
282
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
283
+
284
+ # Check if it was actually installed or already satisfied
285
+ if "successfully installed" in result.stdout.lower():
286
+ print(f"✓ Successfully installed {extension}")
287
+ elif "already satisfied" in result.stdout.lower():
288
+ print(f"✓ {extension} is already installed")
289
+ else:
290
+ print(f"✓ {extension} installation completed")
291
+
292
+ return True
293
+
294
+ except subprocess.CalledProcessError as e:
295
+ print(f"✗ Failed to install {extension}")
296
+ if e.stderr:
297
+ print(f"Error: {e.stderr.strip()}")
298
+ return False
299
+
300
+ def uninstall_extensions(self, extensions: List[str]) -> bool:
301
+ """Uninstall extensions"""
302
+ if not extensions:
303
+ print("No extensions specified")
304
+ return False
305
+
306
+ available = self.get_available_packages()
307
+
308
+ # Validate extension names
309
+ invalid_extensions = [ext for ext in extensions if ext not in available]
310
+ if invalid_extensions:
311
+ print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
312
+ return False
313
+
314
+ # Check which extensions are actually installed
315
+ installed = self.get_installed_packages()
316
+ not_installed = [ext for ext in extensions if ext not in installed]
317
+
318
+ if not_installed:
319
+ print(
320
+ f"Warning: These extensions are not installed: {', '.join(not_installed)}"
321
+ )
322
+
323
+ to_uninstall = [ext for ext in extensions if ext in installed]
324
+ if not to_uninstall:
325
+ print("No installed extensions to uninstall")
326
+ return True
327
+
328
+ # Confirm uninstallation
329
+ print(f"This will uninstall: {', '.join(to_uninstall)}")
330
+ response = input("Continue? [y/N]: ").strip().lower()
331
+ if response not in ["y", "yes"]:
332
+ print("Uninstallation cancelled")
333
+ return False
334
+
335
+ success = True
336
+ for extension in to_uninstall:
337
+ if not self._uninstall_single_extension(extension):
338
+ success = False
339
+
340
+ return success
341
+
342
+ def _uninstall_single_extension(self, extension: str) -> bool:
343
+ """Uninstall a single extension"""
344
+ cmd = [sys.executable, "-m", "pip", "uninstall", "-y", extension]
345
+
346
+ try:
347
+ print(f"Uninstalling {extension}...")
348
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
349
+ print(f"✓ Successfully uninstalled {extension}")
350
+ return True
351
+
352
+ except subprocess.CalledProcessError as e:
353
+ print(f"✗ Failed to uninstall {extension}")
354
+ if e.stderr:
355
+ print(f"Error: {e.stderr.strip()}")
356
+ return False