diff --git a/.gitignore b/.gitignore index d5ad8a9..30b51f1 100644 --- a/.gitignore +++ b/.gitignore @@ -125,14 +125,14 @@ dmypy.json pythonlib* -*.blend1 - -blendcache_* - *.ipynb .secrets - .vdb -!scipy_ndimage/*/** \ No newline at end of file +!scipy_ndimage/*/** + +# Blender +*.blend1 +blendcache_* +*.cats.txt~ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d409372 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,222 @@ +# BioxelNodes - Agent Development Guide + +This file contains essential information for agentic coding agents working on the BioxelNodes codebase. + +## Project Overview + +BioxelNodes is a Blender addon for scientific volumetric data visualization. It integrates with Blender's Geometry Nodes and Cycles rendering engine to process and visualize medical/scientific data. + +**Language:** Python 3.11 +**Framework:** Blender Python API (bpy) +**Package Manager:** uv + +## Development Commands + +### Environment Setup +```bash +# Install dependencies +uv sync +``` + +### Build Commands +```bash +# Build for specific platform +uv run build.py # platforms: windows-x64, linux-x64, macos-arm64, macos-x64 +``` + +### Code Quality +```bash +# Format code (autopep8) +autopep8 --in-place --recursive src/ +``` + +### Documentation +```bash +# Serve documentation locally +uv run mkdocs serve + +# Build documentation +uv run mike deploy --push --update-aliases 0.2.x latest +``` + +## Code Style Guidelines + +### Formatting +- **4-space indentation** +- **PEP 8** compliance enforced via autopep8 + +### Naming Conventions +- **snake_case** for functions, variables, and files +- **PascalCase** for classes (Blender convention) +- **UPPER_CASE** for constants +- **bioxel_** prefix for addon-specific properties + +### Import Organization +```python +# Standard library imports first +import pathlib +import uuid + +# Third-party imports +import bpy +import numpy as np +import SimpleITK as sitk + +# Relative imports for internal modules +from ..exceptions import CancelledByUser +from ..utils import get_layer_obj +``` + +### Type Hints +- Use sparingly, mainly for Blender property annotations +- Focus on function signatures and complex data structures +- Example: `def process_data(data: np.ndarray) -> np.ndarray:` + +## Architecture Patterns + +### Auto-Registration System +- Uses `auto_load.py` for automatic class registration +- Classes are discovered automatically via reflection +- Registration order resolved via topological sorting +- All Blender classes must inherit from appropriate base types + +### Module Structure +``` +src/bioxelnodes/ +├── __init__.py # Addon entry point, asset library setup +├── auto_load.py # Auto-registration system +├── constants.py # Constants and paths +├── utils.py # Utility functions +├── preferences.py # Addon preferences +├── props.py # Blender properties +├── panels.py # UI panels +├── menus.py # UI menus +├── node.py # Node utilities +├── layer.py # Layer management +├── operators/ # Blender operators +├── bioxel/ # Core bioxel functionality +└── assets/ # Blender assets and node libraries +``` + +### Blender Integration Patterns + +#### Property Groups +```python +class BioxelProperties(bpy.types.PropertyGroup): + bl_label = "Bioxel Properties" + + bioxel_custom_prop: bpy.props.StringProperty( + name="Custom Property", + default="", + description="Description for UI" + ) +``` + +#### Operators +```python +class BIOXEL_OT_custom_operator(bpy.types.Operator): + bl_idname = "bioxel.custom_operator" + bl_label = "Custom Operator" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + # Operator logic here + return {'FINISHED'} +``` + +#### Panels +```python +class BIOXEL_PT_custom_panel(bpy.types.Panel): + bl_label = "Custom Panel" + bl_idname = "BIOXEL_PT_custom_panel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = "Bioxel" + + def draw(self, context): + layout = self.layout + # UI drawing code here +``` + +## Error Handling + +### Error Handling Patterns +- Use try-catch blocks for file operations +- Handle user cancellation gracefully +- Report progress for long operations using `bioxel_progress_factor` and `bioxel_progress_text` +- Log warnings with print statements for debugging + + +## Key Dependencies + +### Core Libraries +- **bpy** (Blender Python API) - Core integration +- **numpy** - Array processing +- **SimpleITK** - Medical image processing +- **h5py** - HDF5 file handling + +### Scientific Libraries +- **matplotlib** - Plotting and visualization +- **transforms3d** - 3D transformations +- **pyometiff** - OME-TIFF support +- **mrcfile** - MRC file format support + +## Development Workflow + +### Making Changes +1. Identify the appropriate module (operators, panels, bioxel, etc.) +2. Follow existing naming conventions and patterns +3. Use auto-registration - no manual registration needed +4. Test with Blender's Python console +5. Format code with autopep8 before committing + +### Adding New Features +1. Create appropriate classes in relevant modules +2. Use proper prefixes (BIOXEL_OT_, BIOXEL_PT_, etc.) +3. Add to auto_load system automatically discovers new classes +4. Update documentation if needed + +### File Operations +- Use `pathlib.Path` for path operations +- Check file existence before operations +- Handle permissions and missing files gracefully +- Use appropriate file formats (HDF5 for data, PNG for images) + +## Blender Integration Notes + +### Asset Library +- Automatically managed via `add_asset_library_if_missing()` +- Located at `NODE_LIB_DIRPATH` +- Uses "PACK" import method +- Name: "O Bioxel" + +### Progress Reporting +- Use `bioxel_progress_factor` (0.0 to 1.0) for progress +- Use `bioxel_progress_text` for status messages +- Update via WindowManager properties + +### Layer Management +- Layers stored as HDF5 files +- Container objects manage multiple layers +- Use `layer.py` for layer operations +- Support for various medical image formats + +## Common Pitfalls + +### Blender API +- Always check context validity +- Handle different Blender versions (check `bpy.app.version`) +- Use proper property types for UI +- Register classes in correct order (auto_load handles this) + +### Performance +- Use NumPy for array operations +- Avoid expensive operations in UI draw calls +- Cache computed values where appropriate +- Use background threads for long operations + +### File I/O +- Always close file handles +- Use context managers for file operations +- Handle large files in chunks +- Validate file formats before processing \ No newline at end of file diff --git a/build.py b/build.py index d7bc1cb..f0d83c4 100644 --- a/build.py +++ b/build.py @@ -17,7 +17,9 @@ required_packages = ["SimpleITK==2.3.1", "mrcfile==1.5.1", "h5py==3.11.0", "transforms3d==0.4.2", - "tifffile==2024.7.24"] + "tifffile==2024.7.24", + "matplotlib==3.10.7", + "pillow==11.2.1"] platforms = {"windows-x64": Platform(pypi_suffix="win_amd64", diff --git a/pack.py b/pack.py new file mode 100644 index 0000000..5e72506 --- /dev/null +++ b/pack.py @@ -0,0 +1,92 @@ +""" +Create a zip archive of the src/bioxelnodes package with a top-level folder +named `bioxelnodes/`. Excludes specified temporary/cache files and folders. +Does not modify any files in the project. + +Usage: + python build_package.py [output_zip_path] + +Example: + python build_package.py windows-x64 +""" +from pathlib import Path +import sys +import zipfile +import fnmatch +from datetime import datetime + +platforms = { + "windows-x64": "windows-x64", + "linux-x64": "linux-x64", + "macos-arm64": "macos-arm64", + "macos-x64": "macos-x64", +} + +# patterns to exclude (filename matching) +EXCLUDE_FILENAME_PATTERNS = [ + "*.blend1", + "blendcache_*", + "*.cats.txt~", + "*.pyc", + "*.pyo", + "*$py.class", +] + +# directory names to exclude entirely +EXCLUDE_DIR_NAMES = { + "__pycache__", +} + +def should_exclude(path: Path) -> bool: + name = path.name + # exclude directories by name anywhere in the path + if any(part in EXCLUDE_DIR_NAMES for part in path.parts): + return True + # only apply filename patterns to files + if path.is_file(): + for pat in EXCLUDE_FILENAME_PATTERNS: + if fnmatch.fnmatch(name, pat): + return True + return False + +def build_package(platform_name: str, out_zip: Path): + src_root = Path("src") / "bioxelnodes" + if not src_root.exists(): + raise SystemExit(f"Source folder not found: {src_root.resolve()}") + + out_zip.parent.mkdir(parents=True, exist_ok=True) + total = 0 + with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for p in src_root.rglob("*"): + if p.is_dir(): + continue + if should_exclude(p): + continue + # arcname should place files under top-level 'bioxelnodes/...' + rel = p.relative_to(src_root) + arcname = Path("bioxelnodes") / rel + zf.write(p, arcname.as_posix()) + total += 1 + print(f"Packaged {total} files into {out_zip}") + +def main(): + if len(sys.argv) < 2: + print("Usage: python build_package.py [output_zip_path]") + print("Available platforms:", ", ".join(platforms.keys())) + sys.exit(1) + + platform_key = sys.argv[1] + if platform_key not in platforms: + print("Unknown platform:", platform_key) + print("Available platforms:", ", ".join(platforms.keys())) + sys.exit(1) + + tag = platforms[platform_key] + timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") + default_name = f"bioxelnodes-{tag}-{timestamp}.zip" + out_path = Path(sys.argv[2]) if len(sys.argv) >= 3 else Path(default_name) + + build_package(platform_key, out_path) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fefa9cc..05dec56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "mrcfile==1.5.1", "h5py==3.11.0", "transforms3d==0.4.2", + "matplotlib==3.10.7", ] [dependency-groups] @@ -25,6 +26,7 @@ dev = [ "autopep8>=2.3.1,<3", "mike>=2.1.2,<3", "tomlkit>=0.13.0,<0.14", + "pip>=25.3", ] [build-system] diff --git a/src/bioxelnodes/__init__.py b/src/bioxelnodes/__init__.py index 1d34f90..9547761 100644 --- a/src/bioxelnodes/__init__.py +++ b/src/bioxelnodes/__init__.py @@ -1,26 +1,109 @@ import bpy +import bpy.utils.previews as previews + +from .constants import NODE_LIB_DIRPATH, PREVIEW_COLLECTIONS +from .props import _bioxel_layer_items, _update_layer_gallery, _update_snapshot_z -from .props import BIOXELNODES_LayerListUL from . import auto_load from . import menus - auto_load.init() +def add_asset_library_if_missing(): + """检查并添加Asset Library(如果不存在),使用link导入方式""" + # 检查路径是否存在 + if not NODE_LIB_DIRPATH.exists(): + print(f"警告: 节点库路径不存在 - {NODE_LIB_DIRPATH}") + return + + # 转换为字符串路径用于Blender设置 + lib_path_str = str(NODE_LIB_DIRPATH) + + # 检查是否已存在该资产库 + prefs = bpy.context.preferences.filepaths.asset_libraries + for lib in prefs: + if lib.path == lib_path_str: + return # 已存在,无需添加 + + # 添加新的资产库,使用link导入方式 + new_lib = prefs.new() + new_lib.name = "O Bioxel" # 资产库名称 + new_lib.path = lib_path_str # 资产库路径(字符串形式) + new_lib.import_method = "PACK" # 设置导入方式为link + + print(f"Add Bioxel Nodes library: {lib_path_str}") + + +def remove_asset_library_if_exists(): + """Remove the Bioxel asset library entry if it was added (by path or name).""" + # if path missing, nothing to remove + lib_path_str = str(NODE_LIB_DIRPATH) + prefs = bpy.context.preferences.filepaths.asset_libraries + + # iterate over a snapshot of prefs to safely find index + lib_to_remove = None + for i, lib in enumerate(list(prefs)): + try: + if lib.path == lib_path_str: + lib_to_remove = lib + break + except Exception: + continue + + if lib_to_remove is not None: + try: + prefs.remove(lib_to_remove) + print(f"Removed Bioxel Nodes library: {lib_path_str}") + except Exception as e: + print(f"Warning: failed to remove Bioxel node library: {e}") + + def register(): + pcoll = previews.new() + pcoll.layer_previews = () + PREVIEW_COLLECTIONS["layers"] = pcoll + + bpy.types.WindowManager.bioxel_progress_factor = bpy.props.FloatProperty( + default=1.0 + ) + + bpy.types.WindowManager.bioxel_progress_text = bpy.props.StringProperty() + + bpy.types.WindowManager.bioxel_layer_library = bpy.props.EnumProperty( + name="Bioxel Layer Gallery", + items=_bioxel_layer_items, + update=_update_layer_gallery, + ) + + bpy.types.WindowManager.bioxel_snapshot_z = bpy.props.FloatProperty( + name="Snapshot Z", + default=0.5, + min=0.0, + max=1.0, + subtype="FACTOR", + update=_update_snapshot_z, + ) + + bpy.types.WindowManager.bioxel_layer_list_index = bpy.props.IntProperty( + default=0) + auto_load.register() - bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty( - default=1.0) - bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty() - bpy.types.WindowManager.bioxelnodes_layer_list_UL = bpy.props.PointerProperty( - type=BIOXELNODES_LayerListUL) menus.add() + add_asset_library_if_missing() def unregister(): + remove_asset_library_if_exists() menus.remove() - del bpy.types.WindowManager.bioxelnodes_progress_factor - del bpy.types.WindowManager.bioxelnodes_progress_text - del bpy.types.WindowManager.bioxelnodes_layer_list_UL auto_load.unregister() + + del bpy.types.WindowManager.bioxel_progress_factor + del bpy.types.WindowManager.bioxel_progress_text + del bpy.types.WindowManager.bioxel_layer_library + del bpy.types.WindowManager.bioxel_snapshot_z + del bpy.types.WindowManager.bioxel_layer_list_index + + for pcoll in PREVIEW_COLLECTIONS.values(): + previews.remove(pcoll) + PREVIEW_COLLECTIONS.clear() diff --git a/src/bioxelnodes/assets/Nodes/BioxelNodes_latest.blend b/src/bioxelnodes/assets/Nodes/BioxelNodes_latest.blend deleted file mode 100644 index 57528b7..0000000 --- a/src/bioxelnodes/assets/Nodes/BioxelNodes_latest.blend +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a76a54a0ec3a2a279e6cc06288975c23ca0cb999dafd0d3fd8842ec3e310fc41 -size 9057934 diff --git a/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.2.9.blend b/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.2.9.blend deleted file mode 100644 index 016c4cf..0000000 --- a/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.2.9.blend +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b219a6f005718d8223766215a598ebde9b646c3960fb298d72ffc864791f3c3a -size 6691383 diff --git a/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.3.3.blend b/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.3.3.blend deleted file mode 100644 index 595e09b..0000000 --- a/src/bioxelnodes/assets/Nodes/BioxelNodes_v0.3.3.blend +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87831f56b9d23b8ee3eccfea02c27e8ff305723a079e2c115a7b059fb5e1cb24 -size 6803344 diff --git a/src/bioxelnodes/assets/O Bioxel/BioxelNodes.blend b/src/bioxelnodes/assets/O Bioxel/BioxelNodes.blend new file mode 100644 index 0000000..5db983f --- /dev/null +++ b/src/bioxelnodes/assets/O Bioxel/BioxelNodes.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0cdfc0e9a37d0e559691fd8893697e1236ddbf3d31b6165ea72e7dcffada2f +size 14083935 diff --git a/src/bioxelnodes/assets/O Bioxel/blender_assets.cats.txt b/src/bioxelnodes/assets/O Bioxel/blender_assets.cats.txt new file mode 100644 index 0000000..80c7c80 --- /dev/null +++ b/src/bioxelnodes/assets/O Bioxel/blender_assets.cats.txt @@ -0,0 +1,12 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +c0699a8b-6fd6-4d10-bdde-5ea0fa36724d:O Bioxel:O Bioxel +5ea24e10-5a2b-44d1-b67b-19014c412793:O Bioxel/Layer:O Bioxel-Layer +17383d65-f1a0-4122-b73a-0baaad4b8ecf:O Bioxel/Structure:O Bioxel-Structure +d03fc5b0-465c-45ba-ba8e-524a1c52ae3d:O Bioxel/Utilities:O Bioxel-Utilities diff --git a/src/bioxelnodes/auto_load.py b/src/bioxelnodes/auto_load.py index 93501f4..d0dedda 100644 --- a/src/bioxelnodes/auto_load.py +++ b/src/bioxelnodes/auto_load.py @@ -80,11 +80,13 @@ def get_ordered_classes_to_register(modules): def get_register_deps_dict(modules): my_classes = set(iter_my_classes(modules)) - my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} + my_classes_by_idname = { + cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} deps_dict = {} for cls in my_classes: - deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) + deps_dict[cls] = set(iter_my_register_deps( + cls, my_classes, my_classes_by_idname)) return deps_dict @@ -181,7 +183,8 @@ def toposort(deps_dict): sorted_values.add(value) else: unsorted.append(value) - deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted} + deps_dict = {value: deps_dict[value] - + sorted_values for value in unsorted} sorted_list_sub.sort(key=lambda cls: getattr(cls, "bl_order", 0)) sorted_list.extend(sorted_list_sub) return sorted_list diff --git a/src/bioxelnodes/bioxel/layer.py b/src/bioxelnodes/bioxel/layer.py index 4837b51..d9449df 100644 --- a/src/bioxelnodes/bioxel/layer.py +++ b/src/bioxelnodes/bioxel/layer.py @@ -78,8 +78,8 @@ class Layer(): mask_frame = mask[f, :, :, :] if smooth > 0: mask_frame = scipy.median_filter(mask_frame.astype(np.float32), - mode="nearest", - size=smooth) + mode="nearest", + size=smooth) # mask_frame = scipy.median_filter( # mask_frame.astype(np.float32), size=2) mask_frames += (mask_frame,) @@ -88,8 +88,8 @@ class Layer(): mask_frame = mask[:, :, :] if smooth > 0: mask_frame = scipy.median_filter(mask_frame.astype(np.float32), - mode="nearest", - size=smooth) + mode="nearest", + size=smooth) # mask_frame = scipy.median_filter( # mask_frame.astype(np.float32), size=2) mask_frames += (mask_frame,) @@ -150,3 +150,32 @@ class Layer(): mat_scale = transforms3d.zooms.zfdir2aff(factors[0]) self.affine = np.dot(self.affine, mat_scale) + + def snapshot(self, shape: tuple, smooth: int = 0): + if len(shape) != 3: + raise Exception("Shape must be 3 dim") + + data = self.data + + snapshot = data[0, :, :, :, :] + + factors = np.divide(self.shape, shape) + zoom_factors = [1 / f for f in factors] + order = 0 if snapshot.dtype == bool else 1 + snapshot = ndi.zoom(snapshot, + zoom_factors+[1.0], + mode="nearest", + grid_mode=False, + order=order) + + snapshot = snapshot.astype(np.float32) + + if self.kind in ['scalar', 'vector', 'color']: + mn = float(np.nanmin(snapshot)) + mx = float(np.nanmax(snapshot)) + if mx - mn > 1e-8: + snapshot = (snapshot - mn) / (mx - mn) + else: + snapshot = np.clip(snapshot, 0.0, 1.0) + + return snapshot diff --git a/src/bioxelnodes/bioxel/scipy/_filters.py b/src/bioxelnodes/bioxel/scipy/_filters.py index 3bbd8ef..1e54ff4 100644 --- a/src/bioxelnodes/bioxel/scipy/_filters.py +++ b/src/bioxelnodes/bioxel/scipy/_filters.py @@ -39,7 +39,6 @@ from . import _ni_support from . import _nd_image - def _invalid_origin(origin, lenw): return (origin < -(lenw // 2)) or (origin > (lenw - 1) // 2) @@ -73,7 +72,6 @@ def _complex_via_real_components(func, input, weights, output, cval, **kwargs): return output - def correlate1d(input, weights, axis=-1, output=None, mode="reflect", cval=0.0, origin=0): """Calculate a 1-D correlation along the given axis. @@ -133,7 +131,6 @@ def correlate1d(input, weights, axis=-1, output=None, mode="reflect", return output - def convolve1d(input, weights, axis=-1, output=None, mode="reflect", cval=0.0, origin=0): """Calculate a 1-D convolution along the given axis. @@ -205,7 +202,6 @@ def _gaussian_kernel1d(sigma, order, radius): return q * phi_x - def gaussian_filter1d(input, sigma, axis=-1, order=0, output=None, mode="reflect", cval=0.0, truncate=4.0, *, radius=None): """1-D Gaussian filter. @@ -274,7 +270,6 @@ def gaussian_filter1d(input, sigma, axis=-1, order=0, output=None, return correlate1d(input, weights, axis, output, mode, cval, 0) - def gaussian_filter(input, sigma, order=0, output=None, mode="reflect", cval=0.0, truncate=4.0, *, radius=None, axes=None): @@ -381,7 +376,6 @@ def gaussian_filter(input, sigma, order=0, output=None, return output - def prewitt(input, axis=-1, output=None, mode="reflect", cval=0.0): """Calculate a Prewitt filter. @@ -443,7 +437,6 @@ def prewitt(input, axis=-1, output=None, mode="reflect", cval=0.0): return output - def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0): """Calculate a Sobel filter. @@ -501,7 +494,6 @@ def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0): return output - def generic_laplace(input, derivative2, output=None, mode="reflect", cval=0.0, extra_arguments=(), @@ -549,7 +541,6 @@ def generic_laplace(input, derivative2, output=None, mode="reflect", return output - def laplace(input, output=None, mode="reflect", cval=0.0): """N-D Laplace filter based on approximate second derivatives. @@ -584,7 +575,6 @@ def laplace(input, output=None, mode="reflect", cval=0.0): return generic_laplace(input, derivative2, output, mode, cval) - def gaussian_laplace(input, sigma, output=None, mode="reflect", cval=0.0, **kwargs): """Multidimensional Laplace filter using Gaussian second derivatives. @@ -637,7 +627,6 @@ def gaussian_laplace(input, sigma, output=None, mode="reflect", extra_keywords=kwargs) - def generic_gradient_magnitude(input, derivative, output=None, mode="reflect", cval=0.0, extra_arguments=(), extra_keywords=None): @@ -690,7 +679,6 @@ def generic_gradient_magnitude(input, derivative, output=None, return output - def gaussian_gradient_magnitude(input, sigma, output=None, mode="reflect", cval=0.0, **kwargs): """Multidimensional gradient magnitude using Gaussian derivatives. @@ -792,7 +780,6 @@ def _correlate_or_convolve(input, weights, output, mode, cval, origin, return output - def correlate(input, weights, output=None, mode='reflect', cval=0.0, origin=0): """ @@ -856,7 +843,6 @@ def correlate(input, weights, output=None, mode='reflect', cval=0.0, origin, False) - def convolve(input, weights, output=None, mode='reflect', cval=0.0, origin=0): """ @@ -967,7 +953,6 @@ def convolve(input, weights, output=None, mode='reflect', cval=0.0, origin, True) - def uniform_filter1d(input, size, axis=-1, output=None, mode="reflect", cval=0.0, origin=0): """Calculate a 1-D uniform filter along the given axis. @@ -1018,7 +1003,6 @@ def uniform_filter1d(input, size, axis=-1, output=None, return output - def uniform_filter(input, size=3, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): """Multidimensional uniform filter. @@ -1088,7 +1072,6 @@ def uniform_filter(input, size=3, output=None, mode="reflect", return output - def minimum_filter1d(input, size, axis=-1, output=None, mode="reflect", cval=0.0, origin=0): """Calculate a 1-D minimum filter along the given axis. @@ -1145,7 +1128,6 @@ def minimum_filter1d(input, size, axis=-1, output=None, return output - def maximum_filter1d(input, size, axis=-1, output=None, mode="reflect", cval=0.0, origin=0): """Calculate a 1-D maximum filter along the given axis. @@ -1302,7 +1284,6 @@ def _min_or_max_filter(input, size, footprint, structure, output, mode, return output - def minimum_filter(input, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): """Calculate a multidimensional minimum filter. @@ -1350,7 +1331,6 @@ def minimum_filter(input, size=None, footprint=None, output=None, cval, origin, 1, axes) - def maximum_filter(input, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): """Calculate a multidimensional maximum filter. @@ -1398,7 +1378,6 @@ def maximum_filter(input, size=None, footprint=None, output=None, cval, origin, 0, axes) - def _rank_filter(input, rank, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, operation='rank', axes=None): @@ -1491,7 +1470,6 @@ def _rank_filter(input, rank, size=None, footprint=None, output=None, return output - def rank_filter(input, rank, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): """Calculate a multidimensional rank filter. @@ -1535,7 +1513,6 @@ def rank_filter(input, rank, size=None, footprint=None, output=None, origin, 'rank', axes=axes) - def median_filter(input, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): """ @@ -1586,7 +1563,6 @@ def median_filter(input, size=None, footprint=None, output=None, origin, 'median', axes=axes) - def percentile_filter(input, percentile, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, *, axes=None): @@ -1630,7 +1606,6 @@ def percentile_filter(input, percentile, size=None, footprint=None, cval, origin, 'percentile', axes=axes) - def generic_filter1d(input, function, filter_size, axis=-1, output=None, mode="reflect", cval=0.0, origin=0, extra_arguments=(), extra_keywords=None): @@ -1718,7 +1693,6 @@ def generic_filter1d(input, function, filter_size, axis=-1, return output - def generic_filter(input, function, size=None, footprint=None, output=None, mode="reflect", cval=0.0, origin=0, extra_arguments=(), extra_keywords=None): @@ -1846,4 +1820,4 @@ def generic_filter(input, function, size=None, footprint=None, mode = _ni_support._extend_mode_to_code(mode) _nd_image.generic_filter(input, function, footprint, output, mode, cval, origins, extra_arguments, extra_keywords) - return output \ No newline at end of file + return output diff --git a/src/bioxelnodes/bioxel/scipy/_interpolation.py b/src/bioxelnodes/bioxel/scipy/_interpolation.py index 0384130..0479344 100644 --- a/src/bioxelnodes/bioxel/scipy/_interpolation.py +++ b/src/bioxelnodes/bioxel/scipy/_interpolation.py @@ -11,7 +11,7 @@ def _prepad_for_spline_filter(input, mode, cval): npad = 12 if mode == 'grid-constant': padded = np.pad(input, npad, mode='constant', - constant_values=cval) + constant_values=cval) elif mode == 'nearest': padded = np.pad(input, npad, mode='edge') else: @@ -21,6 +21,7 @@ def _prepad_for_spline_filter(input, mode, cval): padded = input return padded, npad + def spline_filter1d(input, order=3, axis=-1, output=np.float64, mode='mirror'): """ diff --git a/src/bioxelnodes/bioxel/scipy/_ni_support.py b/src/bioxelnodes/bioxel/scipy/_ni_support.py index ae8875f..c312772 100644 --- a/src/bioxelnodes/bioxel/scipy/_ni_support.py +++ b/src/bioxelnodes/bioxel/scipy/_ni_support.py @@ -83,7 +83,8 @@ def _get_output(output, input, shape=None, complex_output=False): elif isinstance(output, (type, np.dtype)): # Classes (like `np.float32`) and dtypes are interpreted as dtype if complex_output and np.dtype(output).kind != 'c': - warnings.warn("promoting specified output dtype to complex", stacklevel=3) + warnings.warn( + "promoting specified output dtype to complex", stacklevel=3) output = np.promote_types(output, np.complex64) output = np.zeros(shape, dtype=output) elif isinstance(output, str): diff --git a/src/bioxelnodes/bioxel/scipy/_utils.py b/src/bioxelnodes/bioxel/scipy/_utils.py index c397ef4..5a4b9be 100644 --- a/src/bioxelnodes/bioxel/scipy/_utils.py +++ b/src/bioxelnodes/bioxel/scipy/_utils.py @@ -7,4 +7,4 @@ def normalize_axis_index(axis, ndim): if axis < 0: axis = axis + ndim - return axis \ No newline at end of file + return axis diff --git a/src/bioxelnodes/bioxel/skimage/_warps.py b/src/bioxelnodes/bioxel/skimage/_warps.py index 992e38b..f8f4a4f 100644 --- a/src/bioxelnodes/bioxel/skimage/_warps.py +++ b/src/bioxelnodes/bioxel/skimage/_warps.py @@ -3,6 +3,7 @@ import numpy as np from .. import scipy as ndi from ._utils import convert_to_float + def _clip_warp_output(input_image, output_image, mode, cval, clip): """Clip output image to range of values of input image. diff --git a/src/bioxelnodes/bioxel/skimage/dtype.py b/src/bioxelnodes/bioxel/skimage/dtype.py index 0b69b7b..07bc3b4 100644 --- a/src/bioxelnodes/bioxel/skimage/dtype.py +++ b/src/bioxelnodes/bioxel/skimage/dtype.py @@ -39,7 +39,8 @@ _integer_types = ( np.uintp, np.uintc, ) -_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types} +_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) + for t in _integer_types} dtype_range = { bool: (False, True), np.bool_: (False, True), @@ -175,7 +176,8 @@ def _scale(a, n, m, copy=True): # downscale with precision loss if copy: b = np.empty(a.shape, _dtype_bits(kind, m)) - np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting='unsafe') + np.floor_divide(a, 2 ** (n - m), out=b, + dtype=a.dtype, casting='unsafe') return b else: a //= 2 ** (n - m) @@ -282,7 +284,8 @@ def _convert(image, dtype, force_copy=False, uniform=False): return image if not (dtype_in in _supported_types and dtype_out in _supported_types): - raise ValueError(f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.') + raise ValueError( + f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.') if kind_in in 'ui': imin_in = np.iinfo(dtype_in).min @@ -318,7 +321,8 @@ def _convert(image, dtype, force_copy=False, uniform=False): if not uniform: if kind_out == 'u': - image_out = np.multiply(image, imax_out, dtype=computation_type) + image_out = np.multiply( + image, imax_out, dtype=computation_type) else: image_out = np.multiply( image, (imax_out - imin_out) / 2, dtype=computation_type @@ -327,7 +331,8 @@ def _convert(image, dtype, force_copy=False, uniform=False): np.rint(image_out, out=image_out) np.clip(image_out, imin_out, imax_out, out=image_out) elif kind_out == 'u': - image_out = np.multiply(image, imax_out + 1, dtype=computation_type) + image_out = np.multiply( + image, imax_out + 1, dtype=computation_type) np.clip(image_out, 0, imax_out, out=image_out) else: image_out = np.multiply( diff --git a/src/bioxelnodes/bioxelutils/__init__.py b/src/bioxelnodes/bioxelutils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/bioxelnodes/bioxelutils/common.py b/src/bioxelnodes/bioxelutils/common.py deleted file mode 100644 index b2bb850..0000000 --- a/src/bioxelnodes/bioxelutils/common.py +++ /dev/null @@ -1,270 +0,0 @@ -from ast import literal_eval -from pathlib import Path -import bpy - -from ..constants import LATEST_NODE_LIB_PATH, NODE_LIB_DIRPATH, VERSIONS -from ..utils import get_cache_dir - - -def move_node_to_node(node, target_node, offset=(0, 0)): - node.location.x = target_node.location.x + offset[0] - node.location.y = target_node.location.y + offset[1] - - -def move_node_between_nodes(node, target_nodes, offset=(0, 0)): - xs = [] - ys = [] - for target_node in target_nodes: - xs.append(target_node.location.x) - ys.append(target_node.location.y) - - node.location.x = sum(xs) / len(xs) + offset[0] - node.location.y = sum(ys) / len(ys) + offset[1] - - -def get_node_type(node): - node_type = type(node).__name__ - if node_type == "GeometryNodeGroup": - node_type = node.node_tree.name - - return node_type - - -def get_nodes_by_type(node_group, type_name: str): - return [node for node in node_group.nodes if get_node_type(node) == type_name] - - -def get_container_objs_from_selection(): - container_objs = [] - for obj in bpy.context.selected_objects: - if get_container_obj(obj): - container_objs.append(obj) - - return list(set(container_objs)) - - -def get_container_obj(current_obj): - if current_obj: - if current_obj.get('bioxel_container'): - return current_obj - elif current_obj.get('bioxel_layer'): - parent = current_obj.parent - return parent if parent.get('bioxel_container') else None - return None - - -def get_layer_prop_value(layer_obj: bpy.types.Object, prop_name: str): - node_group = layer_obj.modifiers[0].node_group - layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0] - prop = layer_node.inputs.get(prop_name) - if prop is None: - return None - - value = prop.default_value - - if type(value).__name__ == "bpy_prop_array": - value = tuple(value) - return tuple([int(v) for v in value]) \ - if prop in ["shape"] else value - elif type(value).__name__ == "str": - return str(value) - elif type(value).__name__ == "float": - value = float(value) - return round(value, 2) \ - if prop in ["bioxel_size"] else value - elif type(value).__name__ == "int": - value = int(value) - return value - else: - return value - - -def get_layer_name(layer_obj): - return get_layer_prop_value(layer_obj, "name") - - -def get_layer_kind(layer_obj): - return get_layer_prop_value(layer_obj, "kind") - - -def get_layer_label(layer_obj): - name = get_layer_name(layer_obj) - # kind = get_layer_kind(layer_obj) - - label = f"{name}" - - if is_missing_layer(layer_obj): - return "**MISSING**" + label - elif is_temp_layer(layer_obj): - return "* " + label - else: - return label - - -def is_missing_layer(layer_obj): - cache_filepath = Path(bpy.path.abspath( - layer_obj.data.filepath)).resolve() - return not cache_filepath.is_file() - - -def is_temp_layer(layer_obj): - cache_filepath = Path(bpy.path.abspath( - layer_obj.data.filepath)).resolve() - cache_dirpath = Path(get_cache_dir()) - return cache_dirpath in cache_filepath.parents - - -def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value): - node_group = layer_obj.modifiers[0].node_group - layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0] - layer_node.inputs[prop].default_value = value - - -def get_layer_obj(current_obj: bpy.types.Object): - if current_obj: - if current_obj.get('bioxel_layer') and current_obj.parent: - if current_obj.parent.get('bioxel_container'): - return current_obj - return None - - -def get_container_layer_objs(container_obj: bpy.types.Object): - layer_objs = [] - for obj in bpy.data.objects: - if obj.parent == container_obj and get_layer_obj(obj): - layer_objs.append(obj) - - return layer_objs - - -def get_all_layer_objs(): - layer_objs = [] - for obj in bpy.data.objects: - if get_layer_obj(obj): - layer_objs.append(obj) - - return layer_objs - - -def add_driver(target, target_prop, var_sources, expression): - driver = target.driver_add(target_prop) - is_vector = isinstance(driver, list) - drivers = driver if is_vector else [driver] - - for i, driver in enumerate(drivers): - for j, var_source in enumerate(var_sources): - - source = var_source['source'] - prop = var_source['prop'] - - var = driver.driver.variables.new() - var.name = f"var{j}" - - var.targets[0].id_type = source.id_type - var.targets[0].id = source - var.targets[0].data_path = f'["{prop}"][{i}]'\ - if is_vector else f'["{prop}"]' - - driver.driver.expression = expression - - -def add_direct_driver(target, target_prop, source, source_prop): - drivers = [ - { - "source": source, - "prop": source_prop - } - ] - expression = "var0" - add_driver(target, target_prop, drivers, expression) - - -def read_file_prop(content: str): - props = {} - for line in content.split("\n"): - line = line.replace(" ", "") - p = line.split("=")[0] - if p != "": - v = line.split("=")[-1] - props[p] = v - return props - - -def write_file_prop(props: dict): - lines = [] - for p, v in props.items(): - lines.append(f"{p} = {v}") - return "\n".join(lines) - - -def set_file_prop(prop, value): - if bpy.data.texts.get("BioxelNodes") is None: - bpy.data.texts.new("BioxelNodes") - - props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string()) - props[prop] = value - bpy.data.texts["BioxelNodes"].clear() - bpy.data.texts["BioxelNodes"].write(write_file_prop(props)) - - -def get_file_prop(prop): - if bpy.data.texts.get("BioxelNodes") is None: - bpy.data.texts.new("BioxelNodes") - - props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string()) - return props.get(prop) - - -def get_node_version(): - node_version = get_file_prop("node_version") - return literal_eval(node_version) if node_version else None - - -def is_incompatible(): - node_version = get_node_version() - if node_version is None: - for node_group in bpy.data.node_groups: - if node_group.name.startswith("BioxelNodes"): - return True - else: - addon_version = VERSIONS[0]["node_version"] - if node_version[0] != addon_version[0]\ - or node_version[1] != addon_version[1]: - return True - - return False - - -def get_node_lib_path(node_version): - version_str = "v"+".".join([str(i) for i in list(node_version)]) - lib_filename = f"BioxelNodes_{version_str}.blend" - return Path(NODE_LIB_DIRPATH, - lib_filename).resolve() - - -def local_lib_not_updated(): - addon_version = VERSIONS[0]["node_version"] - addon_lib_path = LATEST_NODE_LIB_PATH - - use_local = False - for node_group in bpy.data.node_groups: - if node_group.name.startswith("BioxelNodes"): - lib = node_group.library - if lib: - lib_path = Path(bpy.path.abspath(lib.filepath)).resolve() - if lib_path != addon_lib_path: - use_local = True - break - - not_update = get_node_version() != addon_version - return use_local and not_update - - -def get_output_node(node_group): - try: - output_node = get_nodes_by_type(node_group, - 'NodeGroupOutput')[0] - except: - output_node = node_group.nodes.new("NodeGroupOutput") - - return output_node diff --git a/src/bioxelnodes/bioxelutils/container.py b/src/bioxelnodes/bioxelutils/container.py deleted file mode 100644 index 59c0b1a..0000000 --- a/src/bioxelnodes/bioxelutils/container.py +++ /dev/null @@ -1,186 +0,0 @@ -import bpy - -from ..bioxel.container import Container -from bpy_extras.io_utils import axis_conversion -from mathutils import Matrix, Vector - - -from .layer import Layer, layer_to_obj, obj_to_layer -from .common import (get_container_layer_objs, - get_layer_prop_value, - get_nodes_by_type, get_output_node, - move_node_to_node) -from .node import add_node_to_graph -from ..utils import get_use_link - -NODE_TYPE = { - "label": "BioxelNodes_MaskByLabel", - "scalar": "BioxelNodes_MaskByThreshold" -} - - -def calc_bbox_verts(origin: tuple, size: tuple): - bbox_origin = Vector( - (origin[0], origin[1], origin[2])) - bbox_size = Vector( - (size[0], size[1], size[2])) - bbox_verts = [ - ( - bbox_origin[0] + 0, - bbox_origin[1] + 0, - bbox_origin[2] + 0 - ), - ( - bbox_origin[0] + 0, - bbox_origin[1] + 0, - bbox_origin[2] + bbox_size[2] - ), - ( - bbox_origin[0] + 0, - bbox_origin[1] + bbox_size[1], - bbox_origin[2] + 0 - ), - ( - bbox_origin[0] + 0, - bbox_origin[1] + bbox_size[1], - bbox_origin[2] + bbox_size[2] - ), - ( - bbox_origin[0] + bbox_size[0], - bbox_origin[1] + 0, - bbox_origin[2] + 0 - ), - ( - bbox_origin[0] + bbox_size[0], - bbox_origin[1] + 0, - bbox_origin[2] + bbox_size[2], - ), - ( - bbox_origin[0] + bbox_size[0], - bbox_origin[1] + bbox_size[1], - bbox_origin[2] + 0, - ), - ( - bbox_origin[0] + bbox_size[0], - bbox_origin[1] + bbox_size[1], - bbox_origin[2] + bbox_size[2], - ), - ] - return bbox_verts - - -def obj_to_container(container_obj: bpy.types.Object): - layer_objs = get_container_layer_objs(container_obj) - layers = [obj_to_layer(obj) for obj in layer_objs] - container = Container(name=container_obj.name, - layers=layers) - return container - - -def add_layers(layers: list[Layer], - container_obj: bpy.types.Object, - cache_dir: str): - - node_group = container_obj.modifiers[0].node_group - output_node = get_output_node(node_group) - - for i, layer in enumerate(layers): - layer_obj = layer_to_obj(layer, container_obj, cache_dir) - fetch_node = add_node_to_graph("FetchLayer", - node_group, - use_link=get_use_link()) - fetch_node.label = get_layer_prop_value(layer_obj, "name") - fetch_node.inputs[0].default_value = layer_obj - - if len(output_node.inputs[0].links) == 0: - node_group.links.new(fetch_node.outputs[0], - output_node.inputs[0]) - move_node_to_node(fetch_node, output_node, (-900, 0)) - else: - move_node_to_node(fetch_node, output_node, (0, -100 * (i+1))) - - return container_obj - - -def container_to_obj(container: Container, - scene_scale: float, - step_size: float, - cache_dir: str): - # Wrapper a Container - - # Make transformation - # (S)uperior -Z -> Y - # (A)osterior Y -> Z - mat_ras2blender = axis_conversion(from_forward='-Z', - from_up='Y', - to_forward='Y', - to_up='Z').to_4x4() - - mat_scene_scale = Matrix.Scale(scene_scale, - 4) - - bpy.ops.mesh.primitive_cube_add(enter_editmode=False, - align='WORLD', - location=(0, 0, 0), - scale=(1, 1, 1)) - - container_obj = bpy.context.active_object - - bbox_verts = calc_bbox_verts((0, 0, 0), container.layers[0].shape) - for i, vert in enumerate(container_obj.data.vertices): - transform = Matrix(container.layers[0].affine) - vert.co = transform @ Vector(bbox_verts[i]) - - container_obj.matrix_world = mat_ras2blender @ mat_scene_scale - container_obj.name = container.name - container_obj.data.name = container.name - container_obj.show_in_front = True - - container_obj.lock_location[0] = True - container_obj.lock_location[1] = True - container_obj.lock_location[2] = True - - container_obj.lock_rotation[0] = True - container_obj.lock_rotation[1] = True - container_obj.lock_rotation[2] = True - - container_obj.lock_scale[0] = True - container_obj.lock_scale[1] = True - container_obj.lock_scale[2] = True - - container_obj['bioxel_container'] = True - container_obj["scene_scale"] = scene_scale - container_obj["step_size"] = step_size - - modifier = container_obj.modifiers.new("GeometryNodes", 'NODES') - node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree') - node_group.interface.new_socket(name="Output", - in_out="OUTPUT", - socket_type="NodeSocketGeometry") - modifier.node_group = node_group - node_group.nodes.new("NodeGroupOutput") - - container_obj = add_layers(container.layers, - container_obj=container_obj, - cache_dir=cache_dir) - - try: - layer_node = get_nodes_by_type( - node_group, "BioxelNodes_FetchLayer")[0] - center_node = add_node_to_graph("ReCenter", - node_group, - use_link=get_use_link()) - - output_node = get_output_node(node_group) - - node_group.links.new(layer_node.outputs[0], - center_node.inputs[0]) - node_group.links.new(center_node.outputs[0], - output_node.inputs[0]) - - move_node_to_node(center_node, layer_node, (300, 0)) - bpy.ops.bioxelnodes.add_slicer('EXEC_DEFAULT') - except: - pass - - return container_obj diff --git a/src/bioxelnodes/bioxelutils/layer.py b/src/bioxelnodes/bioxelutils/layer.py deleted file mode 100644 index f7a0525..0000000 --- a/src/bioxelnodes/bioxelutils/layer.py +++ /dev/null @@ -1,215 +0,0 @@ -import random -import re -import bpy -import numpy as np - -try: - import pyopenvdb as vdb -except: - import openvdb as vdb - -from pathlib import Path -from uuid import uuid4 - -from ..bioxel.layer import Layer -from ..utils import get_use_link -from .node import add_node_to_graph -from .common import (get_layer_prop_value, - move_node_between_nodes) - - -def obj_to_layer(layer_obj: bpy.types.Object): - cache_filepath = Path(bpy.path.abspath(layer_obj.data.filepath)).resolve() - is_sequence = re.search(r'\.\d{4}\.', - cache_filepath.name) is not None - if is_sequence: - cache_path = cache_filepath.parent - data_frames = () - for f in cache_path.iterdir(): - if not f.is_file() or f.suffix != ".vdb": - continue - grids, base_metadata = vdb.readAll(str(f)) - grid = grids[0] - metadata = grid.metadata - if grid["layer_kind"] in ['label', 'scalar']: - data_shape = grid["data_shape"] - else: - data_shape = tuple(list(grid["data_shape"]) + [3]) - data_frame = np.ndarray(data_shape, np.float32) - grid.copyToArray(data_frame) - data_frames += (data_frame,) - data = np.stack(data_frames) - else: - grids, base_metadata = vdb.readAll(str(cache_filepath)) - grid = grids[0] - metadata = grid.metadata - if grid["layer_kind"] in ['label', 'scalar']: - data_shape = grid["data_shape"] - else: - data_shape = tuple(list(grid["data_shape"]) + [3]) - data = np.ndarray(data_shape, np.float32) - grid.copyToArray(data) - data = np.expand_dims(data, axis=0) # expend frame - - name = get_layer_prop_value(layer_obj, "name") \ - or metadata["layer_name"] - kind = get_layer_prop_value(layer_obj, "kind") \ - or metadata["layer_kind"] - affine = metadata["layer_affine"] - dtype = get_layer_prop_value(layer_obj, "dtype") \ - or metadata.get("data_dtype") or "float32" - offset = get_layer_prop_value(layer_obj, "offset") \ - or metadata.get("data_offset") or 0 - - data = data - np.full_like(data, offset) - data = data.astype(dtype) - - if kind in ["scalar", "label"]: - data = np.expand_dims(data, axis=-1) # expend channel - - layer = Layer(data=data, - name=name, - kind=kind, - affine=affine) - - return layer - - -def layer_to_obj(layer: Layer, - container_obj: bpy.types.Object, - cache_dir: str): - - data = layer.data - - # TXYZC > TXYZ - if layer.kind in ['label', 'scalar']: - data = np.amax(data, -1) - - offset = 0 - if layer.kind in ['scalar']: - data = data.astype(np.float32) - orig_min = float(np.min(data)) - if orig_min < 0: - offset = -orig_min - - data = data + np.full_like(data, offset) - - metadata = { - "layer_name": layer.name, - "layer_kind": layer.kind, - "layer_affine": layer.affine.tolist(), - "data_shape": layer.shape, - "data_dtype": layer.data.dtype.str, - "data_offset": offset - } - - layer_display_name = f"{container_obj.name}_{layer.name}" - if layer.frame_count > 1: - print(f"Saving the Cache of {layer.name}...") - vdb_name = str(uuid4()) - sequence_path = Path(cache_dir, vdb_name) - sequence_path.mkdir(parents=True, exist_ok=True) - - cache_filepaths = [] - for f in range(layer.frame_count): - if layer.kind in ['label', 'scalar']: - grid = vdb.FloatGrid() - grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32)) - else: - # color - grid = vdb.Vec3SGrid() - grid.copyFromArray( - data[f, :, :, :, :].copy().astype(np.float32)) - grid.transform = vdb.createLinearTransform( - layer.affine.transpose()) - grid.metadata = metadata - grid.name = layer.kind - - cache_filepath = Path(sequence_path, - f"{vdb_name}.{str(f+1).zfill(4)}.vdb") - vdb.write(str(cache_filepath), grids=[grid]) - cache_filepaths.append(cache_filepath) - - else: - if layer.kind in ['label', 'scalar']: - grid = vdb.FloatGrid() - grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32)) - else: - # color - grid = vdb.Vec3SGrid() - grid.copyFromArray(data[0, :, :, :, :].copy().astype(np.float32)) - grid.transform = vdb.createLinearTransform( - layer.affine.transpose()) - grid.metadata = metadata - grid.name = layer.kind - - print(f"Saving the Cache of {layer.name}...") - cache_filepath = Path(cache_dir, f"{uuid4()}.vdb") - vdb.write(str(cache_filepath), grids=[grid]) - cache_filepaths = [cache_filepath] - - layer_data = bpy.data.volumes.new(layer_display_name) - layer_data.render.space = 'WORLD' - scene_scale = container_obj.get("scene_scale") or 0.01 - step_size = container_obj.get("step_size") or 1 - layer_data.render.step_size = scene_scale * step_size - layer_data.sequence_mode = 'REPEAT' - layer_data.filepath = str(cache_filepaths[0]) - - if layer.frame_count > 1: - layer_data.is_sequence = True - layer_data.frame_duration = layer.frame_count - else: - layer_data.is_sequence = False - - layer_obj = bpy.data.objects.new(layer_display_name, layer_data) - layer_obj['bioxel_layer'] = True - - print(f"Creating Node for {layer.name}...") - modifier = layer_obj.modifiers.new("GeometryNodes", 'NODES') - node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree') - node_group.interface.new_socket(name="Cache", - in_out="INPUT", - socket_type="NodeSocketGeometry") - node_group.interface.new_socket(name="Layer", - in_out="OUTPUT", - socket_type="NodeSocketGeometry") - modifier.node_group = node_group - - layer_node = add_node_to_graph("_Layer", - node_group, - use_link=get_use_link()) - - layer_node.inputs['name'].default_value = layer.name - layer_node.inputs['shape'].default_value = layer.shape - layer_node.inputs['kind'].default_value = layer.kind - - for i in range(layer.affine.shape[1]): - for j in range(layer.affine.shape[0]): - affine_key = f"affine{i}{j}" - layer_node.inputs[affine_key].default_value = layer.affine[j, i] - - layer_node.inputs['unique'].default_value = random.uniform(0, 1) - layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0] - layer_node.inputs['dtype'].default_value = layer.dtype.str - layer_node.inputs['dtype_num'].default_value = layer.dtype.num - layer_node.inputs['frame_count'].default_value = layer.frame_count - layer_node.inputs['channel_count'].default_value = layer.channel_count - layer_node.inputs['offset'].default_value = max(0, -layer.min) - layer_node.inputs['min'].default_value = layer.min - layer_node.inputs['max'].default_value = layer.max - - input_node = node_group.nodes.new("NodeGroupInput") - output_node = node_group.nodes.new("NodeGroupOutput") - - node_group.links.new(input_node.outputs[0], - layer_node.inputs[0]) - node_group.links.new(layer_node.outputs[0], - output_node.inputs[0]) - - move_node_between_nodes( - layer_node, [input_node, output_node]) - - layer_obj.parent = container_obj - - return layer_obj diff --git a/src/bioxelnodes/bioxelutils/node.py b/src/bioxelnodes/bioxelutils/node.py deleted file mode 100644 index 57707db..0000000 --- a/src/bioxelnodes/bioxelutils/node.py +++ /dev/null @@ -1,71 +0,0 @@ -from pathlib import Path -import bpy - -from .common import get_file_prop, set_file_prop -from ..exceptions import NoFound - -from ..constants import LATEST_NODE_LIB_PATH, VERSIONS - - -def get_node_tree(node_type: str, use_link=True): - # unannotate below for local debug in node lib file. - # node_group = bpy.data.node_groups[node_type] - # return node_group - - # added node is always from latest node version - addon_version = VERSIONS[0]["node_version"] - addon_lib_path = LATEST_NODE_LIB_PATH - - if get_file_prop("node_version") is None: - set_file_prop("node_version", addon_version) - - local_lib = None - for node_group in bpy.data.node_groups: - if node_group.name.startswith("BioxelNodes"): - lib = node_group.library - if lib: - lib_path = Path(bpy.path.abspath(lib.filepath)).resolve() - if lib_path != addon_lib_path: - local_lib = lib.filepath - break - - # local lib first - lib_path_str = local_lib or str(addon_lib_path) - - with bpy.data.libraries.load(lib_path_str, - link=use_link, - relative=True) as (data_from, data_to): - data_to.node_groups = [n for n in data_from.node_groups - if n == node_type] - - node_tree = data_to.node_groups[0] - - if node_tree is None: - raise NoFound('No custom node found') - - return node_tree - - -def assign_node_tree(node, node_tree): - node.node_tree = node_tree - node.width = 200.0 - node.name = node_tree.name - return node - - -def add_node_to_graph(node_name: str, node_group, node_label=None, use_link=True): - node_type = f"BioxelNodes_{node_name}" - node_label = node_label or node_name - - # Deselect all nodes first - for node in node_group.nodes: - if node.select: - node.select = False - - node_tree = get_node_tree(node_type, use_link) - node = node_group.nodes.new("GeometryNodeGroup") - assign_node_tree(node, node_tree) - - node.label = node_label - node.show_options = False - return node diff --git a/src/bioxelnodes/blender_manifest.toml b/src/bioxelnodes/blender_manifest.toml index 09239a8..d982f0d 100644 --- a/src/bioxelnodes/blender_manifest.toml +++ b/src/bioxelnodes/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "bioxelnodes" -version = "1.0.9" +version = "2.0.0" name = "Bioxel Nodes" tagline = "For scientific volumetric data visualization in Blender" maintainer = "Ma Nan " @@ -9,12 +9,30 @@ type = "add-on" website = "https://omoolab.github.io/BioxelNodes/latest" tags = ["Geometry Nodes", "Render", "Import-Export"] -blender_version_min = "4.2.0" +blender_version_min = "5.0.0" license = ['SPDX:GPL-3.0-or-later'] copyright = ["2024 OmooLab"] platforms = ["windows-x64"] -wheels = ["./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl", "./wheels/lxml-5.3.2-cp311-cp311-win_amd64.whl", "./wheels/mrcfile-1.5.1-py2.py3-none-any.whl", "./wheels/pyometiff-1.0.0-py3-none-any.whl", "./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", "./wheels/tifffile-2024.7.24-py3-none-any.whl", "./wheels/transforms3d-0.4.2-py3-none-any.whl"] +wheels = [ + "./wheels/contourpy-1.3.3-cp311-cp311-win_amd64.whl", + "./wheels/cycler-0.12.1-py3-none-any.whl", + "./wheels/fonttools-4.60.1-cp311-cp311-win_amd64.whl", + "./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl", + "./wheels/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", + "./wheels/lxml-6.0.2-cp311-cp311-win_amd64.whl", + "./wheels/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", + "./wheels/mrcfile-1.5.1-py2.py3-none-any.whl", + "./wheels/packaging-25.0-py3-none-any.whl", + "./wheels/pillow-11.2.1-cp311-cp311-win_amd64.whl", + "./wheels/pyometiff-1.0.0-py3-none-any.whl", + "./wheels/pyparsing-3.2.5-py3-none-any.whl", + "./wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", + "./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", + "./wheels/six-1.17.0-py2.py3-none-any.whl", + "./wheels/tifffile-2024.7.24-py3-none-any.whl", + "./wheels/transforms3d-0.4.2-py3-none-any.whl", +] [permissions] -files = "Import/export volume data from/to disk" \ No newline at end of file +files = "Import/export volume data from/to disk" diff --git a/src/bioxelnodes/constants.py b/src/bioxelnodes/constants.py index ed6a0dd..322b703 100644 --- a/src/bioxelnodes/constants.py +++ b/src/bioxelnodes/constants.py @@ -1,257 +1,9 @@ from pathlib import Path -VERSIONS = [{"label": "Latest", "node_version": (1, 0, 7)}, - {"label": "v0.3.x", "node_version": (0, 3, 3)}, - {"label": "v0.2.x", "node_version": (0, 2, 9)}] - NODE_LIB_DIRPATH = Path(Path(__file__).parent, - "assets/Nodes").resolve() + "assets/O Bioxel").resolve() -LATEST_NODE_LIB_PATH = lib_path = Path(NODE_LIB_DIRPATH, - "BioxelNodes_latest.blend").resolve() +LATEST_NODE_LIB_PATH = Path(NODE_LIB_DIRPATH, + "BioxelNodes.blend").resolve() -COMPONENT_OUTPUT_NODES = [ - "CutoutByThreshold", - "CutoutByRange", - "CutoutByHue", - "JoinComponent", - "SetProperties", - "SetColor", - "SetColorByColor", - "SetColorByColor", - "SetColorByRamp2", - "SetColorByRamp3", - "SetColorByRamp4", - "SetColorByRamp5", - "Cut", -] - -MENU_ITEMS = [ - { - 'label': 'Component', - 'icon': 'OUTLINER_DATA_VOLUME', - 'items': [ - { - 'label': 'Cutout by Threshold', - 'icon': 'EMPTY_SINGLE_ARROW', - 'name': 'CutoutByThreshold', - 'description': '' - }, - { - 'label': 'Cutout by Range', - 'icon': 'IPO_CONSTANT', - 'name': 'CutoutByRange', - 'description': '' - }, - { - 'label': 'Cutout by Hue', - 'icon': 'COLOR', - 'name': 'CutoutByHue', - 'description': '' - }, - "separator", - { - 'label': 'To Surface', - 'icon': 'MESH_DATA', - 'name': 'ToSurface', - 'description': '' - }, - { - 'label': 'Join Component', - 'icon': 'CONSTRAINT_BONE', - 'name': 'JoinComponent', - 'description': '' - }, - { - 'label': 'Slice', - 'icon': 'TEXTURE', - 'name': 'Slice', - 'description': '' - } - ] - }, - { - 'label': 'Property', - 'icon': 'PROPERTIES', - 'items': [ - { - 'label': 'Set Properties', - 'icon': 'PROPERTIES', - 'name': 'SetProperties', - 'description': '' - }, - "separator", - { - 'label': 'Set Color', - 'icon': 'IPO_SINE', - 'name': 'SetColor', - 'description': '' - }, - { - 'label': 'Set Color by Color', - 'icon': 'IPO_QUINT', - 'name': 'SetColorByColor', - 'description': '' - }, - { - 'label': 'Set Color by Ramp 2', - 'icon': 'IPO_QUAD', - 'name': 'SetColorByRamp2', - 'description': '' - }, - { - 'label': 'Set Color by Ramp 3', - 'icon': 'IPO_CUBIC', - 'name': 'SetColorByRamp3', - 'description': '' - }, - { - 'label': 'Set Color by Ramp 4', - 'icon': 'IPO_QUART', - 'name': 'SetColorByRamp4', - 'description': '' - }, - { - 'label': 'Set Color by Ramp 5', - 'icon': 'IPO_QUINT', - 'name': 'SetColorByRamp5', - 'description': '' - } - ] - }, - { - 'label': 'Surface', - 'icon': 'MESH_DATA', - 'items': [ - { - 'label': 'Membrane Shader', - 'icon': 'NODE_MATERIAL', - 'name': 'AssignMembraneShader', - 'description': '' - }, - { - 'label': 'Solid Shader', - 'icon': 'SHADING_SOLID', - 'name': 'AssignSolidShader', - 'description': '' - }, - { - 'label': 'Slime Shader', - 'icon': 'OUTLINER_DATA_META', - 'name': 'AssignSlimeShader', - 'description': '' - }, - - ] - }, - { - 'label': 'Transform', - 'icon': 'EMPTY_AXIS', - 'items': [ - { - 'label': 'Transform', - 'icon': 'EMPTY_AXIS', - 'name': 'Transform', - 'description': '' - }, - { - 'label': 'Transform Parent', - 'icon': 'ORIENTATION_PARENT', - 'name': 'TransformParent', - 'description': '' - }, - { - 'label': 'ReCenter', - 'icon': 'PROP_CON', - 'name': 'ReCenter', - 'description': '' - }, - { - 'label': 'Copy Transform', - 'icon': 'EMPTY_AXIS', - 'name': 'CopyTransform', - 'description': '' - }, - { - 'label': 'Extract Transform', - 'icon': 'EMPTY_AXIS', - 'name': 'ExtractTransform', - 'description': '' - }, - ] - }, - { - 'label': 'Cut', - 'icon': 'MOD_BEVEL', - 'items': [ - { - 'label': 'Cut', - 'icon': 'MOD_BEVEL', - 'name': 'Cut', - 'description': '' - }, - "separator", - { - 'label': 'Primitive Cutter', - 'icon': 'MOD_LINEART', - 'name': 'PrimitiveCutter', - 'description': '' - }, - { - 'label': 'Object Cutter', - 'icon': 'MESH_PLANE', - 'name': 'ObjectCutter', - 'description': '' - } - ] - }, - { - 'label': 'Extra', - 'icon': 'MODIFIER', - 'items': [ - { - 'label': 'Fetch Mesh', - 'icon': 'OUTLINER_OB_MESH', - 'name': 'FetchMesh', - 'description': '' - }, - { - 'label': 'Fetch Volume', - 'icon': 'OUTLINER_OB_VOLUME', - 'name': 'FetchVolume', - 'description': '' - }, - { - 'label': 'Fetch Shape Wire', - 'icon': 'FILE_VOLUME', - 'name': 'FetchShapeWire', - 'description': '' - }, - { - 'label': 'Fetch Bbox Wire', - 'icon': 'MESH_CUBE', - 'name': 'FetchBboxWire', - 'description': '' - }, - "separator", - { - 'label': 'Inflate', - 'icon': 'OUTLINER_OB_META', - 'name': 'Inflate', - 'description': '' - }, - { - 'label': 'Smooth', - 'icon': 'MOD_SMOOTH', - 'name': 'Smooth', - 'description': '' - }, - { - 'label': 'Remove Small Island', - 'icon': 'FORCE_LENNARDJONES', - 'name': 'RemoveSmallIsland', - 'description': '' - } - ] - } -] +PREVIEW_COLLECTIONS = {} diff --git a/src/bioxelnodes/exceptions.py b/src/bioxelnodes/exceptions.py index d2de041..c0b8cb2 100644 --- a/src/bioxelnodes/exceptions.py +++ b/src/bioxelnodes/exceptions.py @@ -1,22 +1,6 @@ -class CancelledByUser(Exception): - def __init__(self): - message = 'Cancelled by user' - super().__init__(message) - - -class NoContent(Exception): - def __init__(self, message): - super().__init__(message) - self.message = message - - -class NoFound(Exception): - def __init__(self, message): - super().__init__(message) - self.message = message - - -class Incompatible(Exception): - def __init__(self, message): - super().__init__(message) - self.message = message +# This file is kept for backward compatibility +# Custom exceptions have been replaced with built-in exceptions: +# CancelledByUser -> CancelledError +# NoContent -> ValueError +# NoFound -> LookupError +# Incompatible -> ValueError diff --git a/src/bioxelnodes/layer.py b/src/bioxelnodes/layer.py new file mode 100644 index 0000000..c6fb53d --- /dev/null +++ b/src/bioxelnodes/layer.py @@ -0,0 +1,277 @@ +import json +import time +from typing import Any, List, Dict +from pathlib import Path +import matplotlib.pyplot as plt + +import bpy +import numpy as np +import openvdb as vdb + +from .bioxel.layer import Layer +from .utils import ndarray_to_png + +LAYERS_JSON = "bioxel_layers" + + +def cache_layer_data(layer: Layer, cache_path: str): + """ + Cache the given Layer's data as one or more VDB files. + + - For multi-frame layers, writes per-frame VDB files named data.0001.vdb, data.0002.vdb, ... + - For single-frame layers, writes a single data.vdb file. + - Applies basic type handling: + - label/scalar layers collapse channel dimension via max. + - scalar layers are offset to avoid negative values. + - The VDB grids will have their transform set from layer.affine but no additional metadata is written. + + Parameters: + - layer: Layer object containing ndarray data and metadata. + - cache_path: directory path where VDB files will be written (created if missing). + """ + data = layer.data + + # 处理标量/标签类型(去除通道维度) + if layer.kind in ["label", "scalar"]: + data = np.amax(data, -1) + + # 标量类型偏移处理(避免负值) + offset = 0 + if layer.kind in ["scalar"]: + data = data.astype(np.float32) + orig_min = float(np.min(data)) + if orig_min < 0: + offset = -orig_min + data = data + np.full_like(data, offset) + + # 创建缓存目录 + cache_path = Path(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + + # 多帧序列处理 + if layer.frame_count > 1: + for f in range(layer.frame_count): + # 根据图层类型创建VDB网格 + if layer.kind in ["label", "scalar"]: + grid = vdb.FloatGrid() + grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32)) + else: # 颜色类型 + grid = vdb.Vec3SGrid() + grid.copyFromArray( + data[f, :, :, :, :].copy().astype(np.float32)) + + # 仅设置transform,不存储metadata + grid.transform = vdb.createLinearTransform( + layer.affine.transpose()) + grid.name = layer.kind + + # 保存序列帧VDB + data_filepath = cache_path / f"data.{str(f+1).zfill(4)}.vdb" + vdb.write(str(data_filepath), grids=[grid]) + + else: + # 单帧处理 + if layer.kind in ["label", "scalar"]: + grid = vdb.FloatGrid() + grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32)) + else: # 颜色类型 + grid = vdb.Vec3SGrid() + grid.copyFromArray(data[0, :, :, :, :].copy().astype(np.float32)) + + # 仅设置transform,不存储metadata + grid.transform = vdb.createLinearTransform(layer.affine.transpose()) + grid.name = layer.kind + + # 保存单帧VDB + data_filepath = cache_path / "data.vdb" + vdb.write(str(data_filepath), grids=[grid]) + + +def cache_layer_snapshot(layer: Layer, cache_path: str): + """ + Create and save a low-resolution snapshot (numpy .npy) of the layer and generate PNG slices. + + - Saves a 3D numpy ndarray snapshot to /snapshot.npy. + - Then creates per-Z PNGs in the same directory via snapshot_to_pngs. + + Parameters: + - layer: Layer to snapshot (expects Layer.snapshot method). + - cache_path: destination directory where snapshot.npy and PNGs will be created. + """ + cache_path = Path(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + + snapshot_filepath = cache_path / "snapshot.npy" + snapshot = layer.snapshot((64, 64, 32)) + np.save(str(snapshot_filepath), snapshot) + snapshot_to_pngs( + snapshot, str(cache_path), normalize=layer.kind in ["scalar", "vector"] + ) + + if layer.kind in ["scalar", "vector", "color"]: + plt.figure(figsize=(8, 8)) + + def plot_histogram(plt, data, color="black"): + flattened = data.flatten() + x_min = np.percentile(flattened, 1) + x_max = np.percentile(flattened, 99) + + n, bins, _ = plt.hist( + flattened, + bins=50, # 自动根据数据密度调整bins数量(避免空柱) + range=(x_min, x_max), # 只显示核心区域,省略稀疏部分 + alpha=0, + edgecolor="none", # 去除边框,使柱形更紧凑 + ) + bin_centers = 0.5 * (bins[1:] + bins[:-1]) # 计算每个bin的中点 + plt.plot(bin_centers, n, color=color, linewidth=4) # 折线 + plt.fill_between(bin_centers, n, color=color, alpha=0.1) + + if layer.kind == "scalar": + plot_histogram(plt, layer.data[:, :, :, 0], color="black") + elif layer.kind in ["vector", "color"]: + plot_histogram(plt, layer.data[:, :, :, 0], color="tomato") + plot_histogram(plt, layer.data[:, :, :, 1], color="yellowgreen") + plot_histogram(plt, layer.data[:, :, :, 2], color="xkcd:azure") + + plt.yscale("log") + plt.gca().get_yaxis().set_visible(False) + plt.xticks(fontsize=32, rotation=45) # 调整字体大小(数值越大字体越大) + + ax = plt.gca() + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(False) # 隐藏左边框 + + # 调整布局避免标签被截断 + plt.tight_layout() + + # 保存图片到指定路径 + save_path = cache_path / "histogram.png" + plt.savefig(save_path, bbox_inches="tight", pad_inches=0.1) # 减少边缘留白) + + # 关闭图像释放资源 + plt.close() + + +def snapshot_to_pngs(snapshot, cache_path: str, normalize=False): + """ + Convert a 3D/4D snapshot ndarray into per-slice PNG files. + + - snapshot is expected with axes order X, Y, Z, (C optional). + - Writes files snapshot_0.png ... snapshot_{Z-1}.png into cache_path. + - Uses ndarray_to_png to perform the per-slice conversion. + + Parameters: + - snapshot: numpy ndarray (X, Y, Z[, C]) + - cache_path: directory where generated PNGs are stored. + """ + cache_path = Path(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + + if normalize: + mn = float(np.percentile(snapshot, 50)) + mx = float(np.percentile(snapshot, 99)) + if mx - mn > 1e-8: + snapshot = (snapshot - mn) / (mx - mn) + else: + snapshot = np.clip(snapshot, 0.0, 1.0) + + shape = snapshot.shape + for zidx in range(shape[2]): + array = snapshot[:, :, zidx, :] + ndarray_to_png(array, str(cache_path / f"snapshot_{zidx}.png")) + + +def get_layer_caches() -> List[Dict[str, Any]]: + """ + Read the saved layer metadata list from Blender's internal text datablock. + + - Looks for a text datablock named by LAYERS_JSON ("bioxel_layers"). + - Returns the parsed list of layer dictionaries if present; otherwise returns an empty list. + + Returns: + - list of layer metadata dictionaries (possibly empty) + """ + # 检查文本数据块是否存在 + if LAYERS_JSON not in bpy.data.texts: + return [] + + # 加载并解析数据 + layers_text = bpy.data.texts[LAYERS_JSON] + try: + layers_data = json.loads(layers_text.as_string()) + # 确保返回格式为列表 + return layers_data if isinstance(layers_data, list) else [] + except: + return [] + + +def set_layer_caches(layers_data: List[Dict[str, Any]]): + """ + Write the provided list of layer metadata dictionaries into the Blender text datablock. + + - Ensures the LAYERS_JSON text datablock exists, then overwrites it with pretty JSON. + + Parameters: + - layers_data: list of serializable dictionaries describing the layers + """ + # 获取或创建文本数据块 + if LAYERS_JSON not in bpy.data.texts: + bpy.data.texts.new(LAYERS_JSON) + layers_text = bpy.data.texts[LAYERS_JSON] + # 写入数据 + layers_text.clear() + layers_text.write(json.dumps(layers_data, indent=4)) + + +def save_layers_to_json(layers: List[Layer], cache_dir: str) -> List[int]: + """ + Save multiple Layer objects into the internal layers text datablock. + + For each layer: + - Generates a unique cache id. + - Writes VDB files and a low-resolution snapshot (.npy) plus PNG slices under cache_dir//. + - Appends an entry describing the layer into the b i o x e l _ l a y e r s text datablock. + + Returns: + - List of generated cache ids for the saved layers. + """ + existing_data = get_layer_caches() + added_ids = [] + + cache_dir_path = Path(cache_dir) + cache_dir_path.mkdir(parents=True, exist_ok=True) + + for idx, layer in enumerate(layers): + cache_id = str(int(time.time())) + str(idx) + cache_path = cache_dir_path / str(cache_id) + cache_layer_data(layer, cache_path) + cache_layer_snapshot(layer, cache_path) + + # build layer_info + cache_info = { + "id": cache_id, + "name": layer.name, + "kind": layer.kind, + "shape": layer.shape, + "affine": layer.affine.tolist(), + "bioxel_size": layer.bioxel_size[0], + "dtype": layer.dtype.str, + "dtype_num": layer.dtype.num, + "frame_count": layer.frame_count, + "channel_count": layer.channel_count, + "offset": max(0, -layer.min), + "min": layer.min, + "max": layer.max, + "path": bpy.path.abspath(str(cache_path)), + "snapshot_z": 0.5, + } + + existing_data.append(cache_info) + added_ids.append(cache_id) + + set_layer_caches(existing_data) + print( + f"Successfully added {len(added_ids)} layers to the internal text datablock") + return added_ids diff --git a/src/bioxelnodes/menus.py b/src/bioxelnodes/menus.py index 7934fd5..18e1afa 100644 --- a/src/bioxelnodes/menus.py +++ b/src/bioxelnodes/menus.py @@ -1,187 +1,40 @@ import bpy +from .operators.structure import ExtractMesh -from .constants import MENU_ITEMS, VERSIONS -from .node_menu import NodeMenu - -from .bioxelutils.common import (get_container_obj, - get_container_layer_objs, get_layer_label, - get_layer_prop_value, get_node_version, is_incompatible) -from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer, - RemoveSelectedLayers, SaveSelectedLayersCache, - ResampleLayer, SignScalar, CombineLabels, - FillByLabel, FillByThreshold, FillByRange) -from .operators.container import (AddLocator, AddSlicer, ContainerProps, - SaveContainerLayersCache, RemoveContainerMissingLayers, - SaveContainer, LoadContainer, - AddPieCutter, AddPlaneCutter, - AddCylinderCutter, AddCubeCutter, AddSphereCutter, - ExtractBboxWire, ExtractMesh, ExtractShapeWire, - ExtractNodeMesh, ExtractNodeBboxWire, ExtractNodeShapeWire) - -from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor) -from .operators.misc import (AddEeveeEnv, CleanTemp, Help, - ReLinkNodeLib, RemoveAllMissingLayers, - RenderSettingPreset, SaveAllLayersCache, - SaveNodeLib, SliceViewer) - - -class IncompatibleMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_INCOMPATIBLE" - bl_label = "Bioxel Nodes" - - def draw(self, context): - tip_text = "please downgrade addon version." - node_version = get_node_version() - if node_version: - version_str = "v"+".".join([str(i) for i in list(node_version)]) - tip_text = f"please downgrade addon version to {version_str}." - - layout = self.layout - layout.label(text="Incompatible node version detected.") - layout.separator() - layout.label( - text="If you still want to edit this file with bioxel nodes, ") - layout.label(text=tip_text) - layout.separator() - layout.label(text="If this file is archived, " - "please relink node library, ") - layout.label(text="check if it still works, " - "then save node library.") - layout.separator() - layout.menu(ReLinkNodeLibMenu.bl_idname) - layout.operator(SaveNodeLib.bl_idname) - - layout.separator() - layout.menu(DangerZoneMenu.bl_idname) - - layout.separator() - layout.menu(RenderSettingMenu.bl_idname) - - layout.separator() - layout.operator(Help.bl_idname, - icon=Help.bl_icon) - - -class FetchLayerMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_ADD_LAYER" - bl_label = "Fetch Layer" - - def draw(self, context): - container_obj = get_container_obj(bpy.context.active_object) - layer_objs = get_container_layer_objs(container_obj) - layout = self.layout - - for layer_obj in layer_objs: - op = layout.operator(FetchLayer.bl_idname, - text=get_layer_label(layer_obj)) - op.layer_obj_name = layer_obj.name - - -class ExtractFromContainerMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_PICK" - bl_label = "Extract from Container" - - def draw(self, context): - layout = self.layout - layout.operator(ExtractMesh.bl_idname) - layout.operator(ExtractShapeWire.bl_idname) - layout.operator(ExtractBboxWire.bl_idname) - - -class AddCutterMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_CUTTERS" - bl_label = "Add a Cutter" - - def draw(self, context): - layout = self.layout - layout.operator(AddPlaneCutter.bl_idname, - icon=AddPlaneCutter.bl_icon, text="Plane Cutter") - layout.operator(AddCylinderCutter.bl_idname, - icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter") - layout.operator(AddCubeCutter.bl_idname, - icon=AddCubeCutter.bl_icon, text="Cube Cutter") - layout.operator(AddSphereCutter.bl_idname, - icon=AddSphereCutter.bl_icon, text="Sphere Cutter") - layout.operator(AddPieCutter.bl_idname, - icon=AddPieCutter.bl_icon, text="Pie Cutter") +from .operators.io import ImportAsLabel, ImportAsScalar, ImportAsColor +from .operators.misc import ( + CleanTemp, + Help, + RenderSettingPreset, + TogglePhantom, +) class ImportLayerMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_IMPORTLAYER" + bl_idname = "BIOXEL_MT_IMPORTLAYER" bl_label = "Import Volumetric Data (Init)" bl_icon = "FILE_NEW" def draw(self, context): layout = self.layout - layout.operator(ImportAsScalar.bl_idname, - text="as Scalar") - layout.operator(ImportAsLabel.bl_idname, - text="as Label") - layout.operator(ImportAsColor.bl_idname, - text="as Color") - - -class AddLayerMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_ADDLAYER" - bl_label = "Import Volumetric Data (Add)" - bl_icon = "FILE_NEW" - - def draw(self, context): - layout = self.layout - layout.operator(ImportAsScalar.bl_idname, - text="as Scalar") - layout.operator(ImportAsLabel.bl_idname, - text="as Label") - layout.operator(ImportAsColor.bl_idname, - text="as Color") - - -class ModifyLayerMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_MODIFYLAYER" - bl_label = "Modify Layer" - bl_icon = "FILE_NEW" - - def draw(self, context): - layout = self.layout - layout.operator(SignScalar.bl_idname, - icon=SignScalar.bl_icon) - layout.operator(FillByThreshold.bl_idname, - icon=FillByThreshold.bl_icon) - layout.operator(FillByRange.bl_idname, - icon=FillByRange.bl_icon) - layout.operator(FillByLabel.bl_idname, - icon=FillByLabel.bl_icon) - layout.operator(CombineLabels.bl_idname, - icon=CombineLabels.bl_icon) - - -class ReLinkNodeLibMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_RELINK" - bl_label = "Relink Node Library" - - def draw(self, context): - layout = self.layout - - for index, version in enumerate(VERSIONS): - op = layout.operator(ReLinkNodeLib.bl_idname, - text=version["label"]) - op.index = index + layout.operator(ImportAsScalar.bl_idname, text="as Scalar") + layout.operator(ImportAsLabel.bl_idname, text="as Label") + layout.operator(ImportAsColor.bl_idname, text="as Color") class RenderSettingMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_RENDER" + bl_idname = "BIOXEL_MT_RENDER" bl_label = "Render Setting Presets" def draw(self, context): layout = self.layout for k, v in RenderSettingPreset.PRESETS.items(): - op = layout.operator(RenderSettingPreset.bl_idname, - text=v) + op = layout.operator(RenderSettingPreset.bl_idname, text=v) op.preset = k class DangerZoneMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_DANGER" + bl_idname = "BIOXEL_MT_DANGER" bl_label = "Danger Zone" def draw(self, context): @@ -190,118 +43,44 @@ class DangerZoneMenu(bpy.types.Menu): class BioxelNodesTopbarMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_TOPBAR" + bl_idname = "BIOXEL_MT_TOPBAR" bl_label = "Bioxel Nodes" def draw(self, context): layout = self.layout - layout.menu(ImportLayerMenu.bl_idname, - icon=ImportLayerMenu.bl_icon) - layout.operator(LoadContainer.bl_idname) - - layout.separator() - layout.operator(SaveAllLayersCache.bl_idname) - layout.operator(RemoveAllMissingLayers.bl_idname) - - layout.separator() - layout.menu(ReLinkNodeLibMenu.bl_idname) - layout.operator(SaveNodeLib.bl_idname) - layout.separator() layout.menu(DangerZoneMenu.bl_idname) layout.separator() - layout.operator(AddEeveeEnv.bl_idname) - layout.menu(RenderSettingMenu.bl_idname) - - layout.separator() - layout.operator(Help.bl_idname, - icon=Help.bl_icon) - - -def TOPBAR(self, context): - layout = self.layout - - if is_incompatible(): - layout.menu(IncompatibleMenu.bl_idname) - else: - layout.menu(BioxelNodesTopbarMenu.bl_idname) - - -class NodeHeadMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_NODE_HEAD" - bl_label = "Bioxel Nodes" - bl_icon = "FILE_VOLUME" - - def draw(self, context): - - layout = self.layout - layout.menu(AddLayerMenu.bl_idname, - icon=AddLayerMenu.bl_icon) - layout.operator(SaveContainer.bl_idname) - - layout.separator() - layout.operator(ContainerProps.bl_idname) - layout.menu(ExtractFromContainerMenu.bl_idname) - - layout.separator() - layout.operator(SaveContainerLayersCache.bl_idname, - icon=SaveContainerLayersCache.bl_icon) - layout.operator(RemoveContainerMissingLayers.bl_idname) - - layout.separator() - layout.operator(AddLocator.bl_idname, - icon=AddLocator.bl_icon) - layout.operator(AddSlicer.bl_idname, - icon=AddSlicer.bl_icon) - layout.menu(AddCutterMenu.bl_idname) - - layout.separator() - layout.menu(FetchLayerMenu.bl_idname) + layout.operator(Help.bl_idname, icon=Help.bl_icon) class NodeContextMenu(bpy.types.Menu): - bl_idname = "BIOXELNODES_MT_NODE_CONTEXT" + bl_idname = "BIOXEL_MT_NODE_CONTEXT" bl_label = "Bioxel Nodes" bl_icon = "FILE_VOLUME" def draw(self, context): layout = self.layout - layout.operator(ExtractNodeMesh.bl_idname) - layout.operator(ExtractNodeShapeWire.bl_idname) - layout.operator(ExtractNodeBboxWire.bl_idname) + layout.operator(ExtractMesh.bl_idname) - layout.separator() - layout.operator(SaveSelectedLayersCache.bl_idname, - icon=SaveSelectedLayersCache.bl_icon) - layout.operator(RemoveSelectedLayers.bl_idname) - layout.separator() - layout.operator(RenameLayer.bl_idname, - icon=RenameLayer.bl_icon) - layout.operator(RetimeLayer.bl_idname) - layout.operator(RelocateLayer.bl_idname) +class ObjectContextMenu(bpy.types.Menu): + bl_idname = "BIOXEL_MT_OBJECT_CONTEXT" + bl_label = "Bioxel Nodes" + bl_icon = "FILE_VOLUME" - layout.separator() - layout.operator(ResampleLayer.bl_idname, - icon=ResampleLayer.bl_icon) - layout.operator(SignScalar.bl_idname) - layout.operator(FillByThreshold.bl_idname) - layout.operator(FillByRange.bl_idname) - layout.operator(FillByLabel.bl_idname) - layout.operator(CombineLabels.bl_idname) + def draw(self, context): + layout = self.layout + layout.operator(TogglePhantom.bl_idname) def NODE_CONTEXT(self, context): - if is_incompatible(): - return - container_obj = context.object is_geo_nodes = context.area.ui_type == "GeometryNodeTree" - is_container = get_container_obj(container_obj) - if not is_geo_nodes or not is_container: + if not is_geo_nodes or not container_obj: return layout = self.layout @@ -309,127 +88,18 @@ def NODE_CONTEXT(self, context): layout.menu(NodeContextMenu.bl_idname) -def NODE_HEAD(self, context): - if is_incompatible(): - return - - container_obj = context.object - is_geo_nodes = context.area.ui_type == "GeometryNodeTree" - is_container = get_container_obj(container_obj) - - if not is_geo_nodes or not is_container: - return - +# 定义添加到右键菜单的函数,放在首位 +def OBJECT_CONTEXT(self, context): layout = self.layout layout.separator() - layout.menu(NodeHeadMenu.bl_idname) - - -def NODE_PROP(self, context): - if is_incompatible(): - return - - container_obj = context.object - is_geo_nodes = context.area.ui_type == "GeometryNodeTree" - is_container = get_container_obj(container_obj) - self.bl_label = "Group" - - if not is_geo_nodes or not is_container: - return - - if container_obj.modifiers[0].node_group != context.space_data.edit_tree: - return - - self.bl_label = "Bioxel Nodes" - - layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL - layer_list = layer_list_UL.layer_list - layer_list.clear() - - for layer_obj in get_container_layer_objs(container_obj): - layer_item = layer_list.add() - layer_item.label = get_layer_label(layer_obj) - layer_item.obj_name = layer_obj.name - layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}" - for prop in ["kind", - "bioxel_size", - "shape", - "frame_count", - "channel_count", - "min", "max"]]) - - layout = self.layout - layout.label(text="Layer List") - split = layout.row() - split.template_list(listtype_name="BIOXELNODES_UL_layer_list", - list_id="layer_list", - dataptr=layer_list_UL, - propname="layer_list", - active_dataptr=layer_list_UL, - active_propname="layer_list_active", - item_dyntip_propname="info_text", - rows=20) - - # sidebar = split.column(align=True) - # sidebar.menu(AddLayerMenu.bl_idname, - # icon=AddLayerMenu.bl_icon, text="") - - # sidebar.operator(SaveContainerLayersCache.bl_idname, - # icon=SaveContainerLayersCache.bl_icon, text="") - # sidebar.operator(RemoveContainerMissingLayers.bl_idname, - # icon=RemoveContainerMissingLayers.bl_icon, text="") - - # sidebar.separator() - # sidebar.operator(SaveSelectedLayersCache.bl_idname, - # icon=SaveSelectedLayersCache.bl_icon, text="") - # sidebar.operator(RemoveSelectedLayers.bl_idname, - # icon=RemoveSelectedLayers.bl_icon, text="") - - # sidebar.separator() - # sidebar.operator(RenameLayer.bl_idname, - # icon=RenameLayer.bl_icon, text="") - # sidebar.operator(ResampleLayer.bl_idname, - # icon=ResampleLayer.bl_icon, text="") - # sidebar.operator(RetimeLayer.bl_idname, - # icon=RetimeLayer.bl_icon, text="") - - # sidebar.separator() - # sidebar.operator(SignScalar.bl_idname, - # icon=SignScalar.bl_icon, text="") - # sidebar.operator(FillByThreshold.bl_idname, - # icon=FillByThreshold.bl_icon, text="") - # sidebar.operator(FillByRange.bl_idname, - # icon=FillByRange.bl_icon, text="") - # sidebar.operator(FillByLabel.bl_idname, - # icon=FillByLabel.bl_icon, text="") - - # sidebar.separator() - # layout.separator() - - -def VIEW3D_TOPBAR(self, context): - layout = self.layout - layout.operator(SliceViewer.bl_idname) - - -node_menu = NodeMenu( - menu_items=MENU_ITEMS -) + layout.menu(ObjectContextMenu.bl_idname) def add(): - node_menu.register() - # bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR) - bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR) - bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PROP) - bpy.types.NODE_MT_editor_menus.append(NODE_HEAD) - bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT) + bpy.types.VIEW3D_MT_object_context_menu.append(OBJECT_CONTEXT) + # bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT) def remove(): - # bpy.types.VIEW3D_HT_header.remove(VIEW3D_TOPBAR) - bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR) - bpy.types.NODE_PT_node_tree_properties.remove(NODE_PROP) - bpy.types.NODE_MT_editor_menus.remove(NODE_HEAD) - bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT) - node_menu.unregister() + bpy.types.VIEW3D_MT_object_context_menu.remove(OBJECT_CONTEXT) + # bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT) diff --git a/src/bioxelnodes/node.py b/src/bioxelnodes/node.py new file mode 100644 index 0000000..566bee6 --- /dev/null +++ b/src/bioxelnodes/node.py @@ -0,0 +1,149 @@ +from pathlib import Path +import bpy + +# LookupError replaced with built-in LookupError +from .constants import LATEST_NODE_LIB_PATH + + +def move_node_to_node(node, target_node, offset=(0, 0)): + node.location.x = target_node.location.x + offset[0] + node.location.y = target_node.location.y + offset[1] + + +def move_node_between_nodes(node, target_nodes, offset=(0, 0)): + xs = [] + ys = [] + for target_node in target_nodes: + xs.append(target_node.location.x) + ys.append(target_node.location.y) + + node.location.x = sum(xs) / len(xs) + offset[0] + node.location.y = sum(ys) / len(ys) + offset[1] + + +def get_node_type(node): + node_type = type(node).__name__ + if node_type == "GeometryNodeGroup": + node_type = node.node_tree.name + + return node_type + + +def get_nodes_by_type(node_group, type_name: str): + return [node for node in node_group.nodes if get_node_type(node) == type_name] + + +def get_node_tree(node_type: str, use_link=True): + try: + return bpy.data.node_groups[node_type] + except KeyError: + lib_path_str = str(LATEST_NODE_LIB_PATH) + with bpy.data.libraries.load(lib_path_str, link=use_link, relative=True) as ( + data_from, + data_to, + ): + data_to.node_groups = [ + n for n in data_from.node_groups if n == node_type] + node_tree = data_to.node_groups[0] + + if node_tree is None: + raise LookupError("No custom node found") + + return node_tree + + +def assign_node_tree(node, node_tree): + node.node_tree = node_tree + node.width = 200.0 + node.name = node_tree.name + return node + + +def add_node_to_graph(node_name: str, node_group, node_label=None, use_link=True): + node_type = f"O {node_name}" + node_label = node_label or node_name + + # Deselect all nodes first + for node in node_group.nodes: + if node.select: + node.select = False + + node_tree = get_node_tree(node_type, use_link) + node = node_group.nodes.new("GeometryNodeGroup") + assign_node_tree(node, node_tree) + + node.label = node_label + node.show_options = False + bpy.ops.node.view_selected() + return node + + +def get_output_node(node_group): + try: + output_node = get_nodes_by_type(node_group, "NodeGroupOutput")[0] + except: + output_node = node_group.nodes.new("NodeGroupOutput") + + return output_node + + +def add_bioxel_node(name: str): + # bpy.ops.node.add_group_asset( + # asset_library_type="CUSTOM", + # asset_library_identifier = "O Bioxel", + # relative_asset_identifier = "NodeTreO Crop Layer" + # ) + # bpy.ops.node.add_group_asset( + # asset_library_type="CUSTOM", + # asset_library_identifier="O Bioxel", + # relative_asset_identifier="Nodes.blend\\NodeTree\\O Render Structure" + # ) + + try: + node_tree = bpy.data.node_groups[name] + except KeyError: + bpy.ops.object.modifier_add_node_group( + asset_library_type="CUSTOM", + asset_library_identifier="O Bioxel", + relative_asset_identifier=f"Nodes.blend\\NodeTree\\{name}", + ) + bpy.ops.object.modifier_remove(modifier=name) + try: + node_tree = bpy.data.node_groups[name] + except Exception as e: + raise e + + bpy.ops.node.add_node( + "INVOKE_DEFAULT", type="GeometryNodeGroup", use_transform=True + ) + + node = bpy.context.active_node + node.node_tree = node_tree + node.show_options = False + + return bpy.context.active_node + + +def get_layer_nodes(node_group): + """Return all O Layer nodes in the given node_tree.""" + return [ + n + for n in node_group.nodes + if n.bl_idname == "GeometryNodeGroup" + and getattr(n, "node_tree", None) + and n.node_tree.name.startswith("O Layer") + ] + + +def get_main_node_group(context): + space = getattr(context, "space_data", None) or getattr( + context, "space", None) + if not space: + return None + node_tree = getattr(space, "node_tree", None) + if not node_tree: + return None + # accept GeometryNodeTree by bl_idname or class name for compatibility + if getattr(node_tree, "bl_idname", "") == "GeometryNodeTree" or node_tree.__class__.__name__ == "GeometryNodeTree": + return node_tree + return None diff --git a/src/bioxelnodes/node_menu.py b/src/bioxelnodes/node_menu.py deleted file mode 100644 index f64cd5b..0000000 --- a/src/bioxelnodes/node_menu.py +++ /dev/null @@ -1,87 +0,0 @@ -import bpy -from .operators.node import AddNode - - -class NodeMenu(): - def __init__( - self, - menu_items, - ) -> None: - - self.menu_items = menu_items - self.class_prefix = f"BIOXELNODES_MT" - root_label = "Bioxel Nodes" - menu_classes = [] - self._create_menu_class( - items=menu_items, - label=root_label, - menu_classes=menu_classes - ) - self.menu_classes = menu_classes - - idname = f"{self.class_prefix}_{root_label.replace(' ', '').upper()}" - icon = "FILE_VOLUME" - - def drew_menu(self, context): - if (bpy.context.area.spaces[0].tree_type == 'GeometryNodeTree'): - layout = self.layout - layout.separator() - layout.menu(idname, - icon=icon) - self.drew_menu = drew_menu - - def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None): - idname_namespace = idname_namespace or self.class_prefix - idname = f"{idname_namespace}_{label.replace(' ', '').upper()}" - - # create submenu class if item is menu. - for item in items: - item_items = item.get('items') if item != 'separator' else None - if item_items: - menu_class = self._create_menu_class( - menu_classes=menu_classes, - items=item_items, - label=item.get('label') or 'CustomNodes', - icon=item.get('icon') or 0, - idname_namespace=idname - ) - item['menu_class'] = menu_class - - # create menu class - class Menu(bpy.types.Menu): - bl_idname = idname - bl_label = label - - def draw(self, context): - layout = self.layout - for item in items: - if item == "separator": - layout.separator() - elif item.get('menu_class'): - layout.menu( - item.get('menu_class').bl_idname, - icon=item.get('icon') or 'NONE' - ) - else: - op = layout.operator( - AddNode.bl_idname, - text=item.get('label'), - icon=item.get('icon') or 'NONE' - ) - op.node_name = item['name'] - op.node_label = item.get('label') or "" - op.node_description = item.get( - 'node_description') or "" - - menu_classes.append(Menu) - return Menu - - def register(self): - for cls in self.menu_classes: - bpy.utils.register_class(cls) - bpy.types.NODE_MT_add.append(self.drew_menu) - - def unregister(self): - bpy.types.NODE_MT_add.remove(self.drew_menu) - for cls in reversed(self.menu_classes): - bpy.utils.unregister_class(cls) diff --git a/src/bioxelnodes/operators/container.py b/src/bioxelnodes/operators/container.py deleted file mode 100644 index d4dad97..0000000 --- a/src/bioxelnodes/operators/container.py +++ /dev/null @@ -1,649 +0,0 @@ -import bpy - - -import bmesh - -from ..constants import COMPONENT_OUTPUT_NODES -from ..utils import get_cache_dir, get_use_link, select_object -from ..bioxel.io import load_container, save_container -from ..bioxelutils.container import container_to_obj, obj_to_container -from ..bioxelutils.node import add_node_to_graph -from ..bioxelutils.common import (get_container_layer_objs, get_container_obj, get_node_type, - get_nodes_by_type, get_output_node, is_missing_layer, - move_node_between_nodes, - move_node_to_node, - get_all_layer_objs) -from .layer import RemoveLayers, SaveLayersCache - - -class SaveContainer(bpy.types.Operator): - bl_idname = "bioxelnodes.save_container" - bl_label = "Save Container (.bioxel) (BETA)" - bl_description = "Clean all caches saved in temp" - - filepath: bpy.props.StringProperty( - subtype="FILE_PATH" - ) # type: ignore - - filename_ext = ".bioxel" - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - container = obj_to_container(container_obj) - save_path = f"{self.filepath.split('.')[0]}.bioxel" - save_container(container, save_path, overwrite=True) - - self.report({"INFO"}, f"Successfully save to {save_path}") - return {'FINISHED'} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - -class LoadContainer(bpy.types.Operator): - bl_idname = "bioxelnodes.load_container" - bl_label = "Load Container (.bioxel) (BETA)" - bl_description = "Clean all caches saved in temp" - - filepath: bpy.props.StringProperty( - subtype="FILE_PATH" - ) # type: ignore - - filename_ext = ".bioxel" - - def execute(self, context): - load_path = self.filepath - container = load_container(self.filepath) - is_first_import = len(get_all_layer_objs()) == 0 - - container_obj = container_to_obj(container, - scene_scale=0.01, - cache_dir=get_cache_dir()) - select_object(container_obj) - - if is_first_import: - bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT', - preset="preview_c") - - self.report({"INFO"}, f"Successfully load {load_path}") - return {'FINISHED'} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - -class ExtractNodeObject(): - bl_options = {'UNDO'} - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - # nodes = [node for node in context.selected_nodes - # if get_node_type(node).removeprefix("BioxelNodes_") in COMPONENT_OUTPUT_NODES] - - if len(context.selected_nodes) == 0: - self.report({"WARNING"}, "No node selected.") - return {'FINISHED'} - - selected_node = context.selected_nodes[0] - container_node_group = container_obj.modifiers[0].node_group - - container_output_node = get_output_node(container_node_group) - - if len(container_output_node.inputs[0].links) == 0: - pre_socket = None - else: - pre_socket = container_output_node.inputs[0].links[0].from_socket - - container_node_group.links.new(selected_node.outputs[0], - container_output_node.inputs[0]) - - bpy.ops.mesh.primitive_cube_add( - size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - obj = bpy.context.active_object - - obj.name = f"{container_obj.name}_{self.object_type}" - - bpy.ops.node.new_geometry_nodes_modifier() - modifier = obj.modifiers[0] - node_group = modifier.node_group - - output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] - fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}", - node_group, - node_label=f"Fetch {self.object_type}", - use_link=get_use_link()) - - fetch_mesh_node.inputs[0].default_value = container_obj - node_group.links.new(fetch_mesh_node.outputs[0], output_node.inputs[0]) - - for modifier in obj.modifiers: - bpy.ops.object.modifier_apply(modifier=modifier.name) - - obj.data.materials.clear() - - if pre_socket: - container_node_group.links.new(pre_socket, - container_output_node.inputs[0]) - - select_object(obj) - - self.report({"INFO"}, f"Successfully picked") - - return {'FINISHED'} - - -class ExtractNodeMesh(bpy.types.Operator, ExtractNodeObject): - bl_idname = "bioxelnodes.extract_node_mesh" - bl_label = "Extract Mesh" - bl_description = "Extract Mesh" - bl_icon = "OUTLINER_OB_MESH" - object_type = "Mesh" - - -class ExtractNodeShapeWire(bpy.types.Operator, ExtractNodeObject): - bl_idname = "bioxelnodes.extract_node_shape_wire" - bl_label = "Extract Shape Wire" - bl_description = "Extract Shape Wire" - bl_icon = "FILE_VOLUME" - object_type = "ShapeWire" - - -class ExtractNodeBboxWire(bpy.types.Operator, ExtractNodeObject): - bl_idname = "bioxelnodes.extract_node_bbox_wire" - bl_label = "Extract Bbox Wire" - bl_description = "Extract Bbox Wire" - bl_icon = "MESH_CUBE" - object_type = "BboxWire" - - -class ExtractObject(): - bl_options = {'UNDO'} - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - bpy.ops.mesh.primitive_cube_add( - size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - obj = bpy.context.active_object - - obj.name = f"{container_obj.name}_{self.object_type}" - - bpy.ops.node.new_geometry_nodes_modifier() - modifier = obj.modifiers[0] - node_group = modifier.node_group - - output_node = get_output_node(node_group) - - fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}", - node_group, - node_label=f"Fetch {self.object_type}", - use_link=get_use_link()) - - fetch_mesh_node.inputs[0].default_value = container_obj - - if self.object_type == "Mesh": - node_group.links.new( - fetch_mesh_node.outputs[0], output_node.inputs[0]) - else: - curve_to_mesh_node = node_group.nodes.new( - "GeometryNodeCurveToMesh") - node_group.links.new( - fetch_mesh_node.outputs[0], curve_to_mesh_node.inputs[0]) - node_group.links.new( - curve_to_mesh_node.outputs[0], output_node.inputs[0]) - - for modifier in obj.modifiers: - bpy.ops.object.modifier_apply(modifier=modifier.name) - - obj.data.materials.clear() - - select_object(obj) - - self.report({"INFO"}, f"Successfully picked") - - return {'FINISHED'} - - -class ExtractMesh(bpy.types.Operator, ExtractObject): - bl_idname = "bioxelnodes.extract_mesh" - bl_label = "Extract Mesh" - bl_description = "Extract Mesh" - bl_icon = "OUTLINER_OB_MESH" - object_type = "Mesh" - - -class ExtractShapeWire(bpy.types.Operator, ExtractObject): - bl_idname = "bioxelnodes.extract_shape_wire" - bl_label = "Extract Shape Wire" - bl_description = "Extract Shape Wire" - bl_icon = "FILE_VOLUME" - object_type = "ShapeWire" - - -class ExtractBboxWire(bpy.types.Operator, ExtractObject): - bl_idname = "bioxelnodes.extract_bbox_wire" - bl_label = "Extract Bbox Wire" - bl_description = "Extract Bbox Wire" - bl_icon = "MESH_CUBE" - object_type = "BboxWire" - - -class AddCutter(): - bl_options = {'UNDO'} - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - # TODO: do not use operator to create obj - if self.cutter_type == "plane": - bpy.ops.mesh.primitive_plane_add( - size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - elif self.cutter_type == "cylinder": - bpy.ops.mesh.primitive_cylinder_add( - radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - bpy.context.object.rotation_euler[0] = container_obj.rotation_euler[0] - elif self.cutter_type == "cube": - bpy.ops.mesh.primitive_cube_add( - size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - elif self.cutter_type == "sphere": - bpy.ops.mesh.primitive_ico_sphere_add( - radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - elif self.cutter_type == "pie": - bpy.ops.mesh.primitive_plane_add( - size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) - pie_obj = bpy.context.active_object - orig_data = pie_obj.data - - # Create mesh - pie_mesh = bpy.data.meshes.new('Pie') - pie_obj.data = pie_mesh - bpy.data.meshes.remove(orig_data) - - # # Create object - # pie = bpy.data.objects.new('Pie', pie_mesh) - - # # Link object to scene - # bpy.context.scene.collection.objects.link(pie) - - # Get a BMesh representation - bm = bmesh.new() # create an empty BMesh - bm.from_mesh(pie_mesh) # fill it in from a Mesh - - # Hot to create vertices - v_0 = bm.verts.new((0.0, -1.0, 0.0)) - v_1 = bm.verts.new((-1.0, -1.0, 1.0)) - v_2 = bm.verts.new((0.0, 1.0, 0.0)) - v_3 = bm.verts.new((-1.0, 1.0, 1.0)) - v_4 = bm.verts.new((1.0, -1.0, 1.0)) - v_5 = bm.verts.new((1.0, 1.0, 1.0)) - - # Initialize the index values of this sequence. - bm.verts.index_update() - - # How to create a face - # it's not necessary to create the edges before, I made it only to show how create - # edges too - bm.faces.new((v_0, v_1, v_3, v_2)) - bm.faces.new((v_0, v_2, v_5, v_4)) - - # Finish up, write the bmesh back to the mesh - bm.to_mesh(pie_mesh) - # bpy.context.view_layer.objects.active = pie - - cutter_obj = bpy.context.active_object - - # vcos = [container_obj.matrix_world @ - # v.co for v in container_obj.data.vertices] - - # def find_center(l): return (max(l) + min(l)) / 2 - - # x, y, z = [[v[i] for v in vcos] for i in range(3)] - # center = [find_center(axis) for axis in [x, y, z]] - - # cutter_obj.location = center - name = f"{self.cutter_type.capitalize()}_Cutter" - cutter_obj.name = name - cutter_obj.data.name = name - cutter_obj.visible_camera = False - cutter_obj.visible_diffuse = False - cutter_obj.visible_glossy = False - cutter_obj.visible_transmission = False - cutter_obj.visible_volume_scatter = False - cutter_obj.visible_shadow = False - cutter_obj.hide_render = True - cutter_obj.display_type = 'WIRE' - cutter_obj.lineart.usage = 'EXCLUDE' - - select_object(container_obj) - - modifier = container_obj.modifiers[0] - node_group = modifier.node_group - - cut_nodes = get_nodes_by_type(node_group, - 'BioxelNodes_Cut') - if len(cut_nodes) == 0: - cutter_node = add_node_to_graph("ObjectCutter", - node_group, - node_label=f"{self.cutter_type.capitalize()} Cutter", - use_link=get_use_link()) - cutter_node.inputs[0].default_value = self.cutter_type.capitalize() - cutter_node.inputs[1].default_value = cutter_obj - - cut_node = add_node_to_graph("Cut", - node_group, - use_link=get_use_link()) - - output_node = get_output_node(node_group) - - if len(output_node.inputs[0].links) == 0: - node_group.links.new(cut_node.outputs[0], - output_node.inputs[0]) - move_node_to_node(cut_node, output_node, (-300, 0)) - else: - pre_output_node = output_node.inputs[0].links[0].from_node - node_group.links.new(pre_output_node.outputs[0], - cut_node.inputs[0]) - node_group.links.new(cut_node.outputs[0], - output_node.inputs[0]) - move_node_between_nodes(cut_node, - [pre_output_node, output_node]) - - node_group.links.new(cutter_node.outputs[0], - cut_node.inputs[1]) - - move_node_to_node(cutter_node, cut_node, (-300, -300)) - else: - bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT', - node_name="ObjectCutter", - node_label=name) - node = bpy.context.active_node - node.inputs[0].default_value = self.cutter_type.capitalize() - node.inputs[1].default_value = cutter_obj - - return {'FINISHED'} - - -class AddPlaneCutter(bpy.types.Operator, AddCutter): - bl_idname = "bioxelnodes.add_plane_cutter" - bl_label = "Add a Plane Cutter" - bl_description = "Add a plane cutter to current container" - bl_icon = "MESH_PLANE" - cutter_type = "plane" - - -class AddCylinderCutter(bpy.types.Operator, AddCutter): - bl_idname = "bioxelnodes.add_cylinder_cutter" - bl_label = "Add a Cylinder Cutter" - bl_description = "Add a cylinder cutter to current container" - bl_icon = "MESH_CYLINDER" - cutter_type = "cylinder" - - -class AddCubeCutter(bpy.types.Operator, AddCutter): - bl_idname = "bioxelnodes.add_cube_cutter" - bl_label = "Add a Cube Cutter" - bl_description = "Add a cube cutter to current container" - bl_icon = "MESH_CUBE" - cutter_type = "cube" - - -class AddSphereCutter(bpy.types.Operator, AddCutter): - bl_idname = "bioxelnodes.add_sphere_cutter" - bl_label = "Add a Sphere Cutter" - bl_description = "Add a sphere cutter to current container" - bl_icon = "MESH_UVSPHERE" - cutter_type = "sphere" - - -class AddPieCutter(bpy.types.Operator, AddCutter): - bl_idname = "bioxelnodes.add_pie_cutter" - bl_label = "Add a Pie Cutter" - bl_description = "Add a pie cutter to current container" - bl_icon = "MESH_CONE" - cutter_type = "pie" - - -class AddSlicer(bpy.types.Operator): - bl_idname = "bioxelnodes.add_slicer" - bl_label = "Add a Slicer" - bl_description = "Add a slicer to current container" - bl_icon = "TEXTURE" - bl_options = {'UNDO'} - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - bpy.ops.mesh.primitive_plane_add(size=2, - enter_editmode=False, - align='WORLD', - location=(0, 0, 0), - scale=(1, 1, 1)) - - slicer_obj = bpy.context.active_object - slicer_obj.name = "Slicer" - slicer_obj.data.name = "Slicer" - - slicer_obj.visible_camera = False - slicer_obj.visible_diffuse = False - slicer_obj.visible_glossy = False - slicer_obj.visible_transmission = False - slicer_obj.visible_volume_scatter = False - slicer_obj.visible_shadow = False - slicer_obj.hide_render = True - slicer_obj.display_type = 'WIRE' - slicer_obj.lineart.usage = 'EXCLUDE' - - select_object(container_obj) - - modifier = container_obj.modifiers[0] - node_group = modifier.node_group - - slice_node = add_node_to_graph("Slice", - node_group, - use_link=get_use_link()) - - slice_node.inputs[1].default_value = slicer_obj - - output_node = get_output_node(node_group) - - if len(output_node.inputs[0].links) == 0: - node_group.links.new(slice_node.outputs[0], - output_node.inputs[0]) - move_node_to_node(slice_node, output_node, (-300, 0)) - else: - pre_output_node = output_node.inputs[0].links[0].from_node - node_group.links.new(pre_output_node.outputs[0], - slice_node.inputs[0]) - node_group.links.new(slice_node.outputs[0], - output_node.inputs[0]) - move_node_between_nodes(slice_node, - [pre_output_node, output_node]) - - return {'FINISHED'} - - -class AddLocator(bpy.types.Operator): - bl_idname = "bioxelnodes.add_locator" - bl_label = "Add a Locator" - bl_description = "Add a locator to current container" - bl_icon = "EMPTY_AXIS" - bl_options = {'UNDO'} - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - bpy.ops.object.empty_add(type='ARROWS', - align='WORLD', - location=(0, 0, 0), - scale=(1, 1, 1)) - - locator_obj = bpy.context.active_object - locator_obj.name = "Locator" - select_object(container_obj) - - modifier = container_obj.modifiers[0] - node_group = modifier.node_group - try: - selected_node = context.selected_nodes[0] - except: - selected_node = None - - parent_node = add_node_to_graph("TransformParent", - node_group, - node_label="Transform Parent", - use_link=get_use_link()) - - parent_node.inputs[1].default_value = locator_obj - - if selected_node is None or len(selected_node.outputs) == 0: - output_node = get_output_node(node_group) - if len(output_node.inputs[0].links) == 0: - node_group.links.new(parent_node.outputs[0], - output_node.inputs[0]) - move_node_to_node(parent_node, output_node, (-300, 0)) - else: - pre_output_node = output_node.inputs[0].links[0].from_node - node_group.links.new(pre_output_node.outputs[0], - parent_node.inputs[0]) - node_group.links.new(parent_node.outputs[0], - output_node.inputs[0]) - move_node_between_nodes(parent_node, - [pre_output_node, output_node]) - else: - try: - to_node = selected_node.outputs[0].links[0].to_node - except: - to_node = None - - if to_node is None: - node_group.links.new(selected_node.outputs[0], - parent_node.inputs[0]) - move_node_to_node(parent_node, selected_node, (300, 0)) - else: - node_group.links.new(selected_node.outputs[0], - parent_node.inputs[0]) - node_group.links.new(parent_node.outputs[0], - to_node.inputs[0]) - move_node_between_nodes(parent_node, - [selected_node, to_node]) - - return {'FINISHED'} - - -class ContainerProps(bpy.types.Operator): - bl_idname = "bioxelnodes.container_props" - bl_label = "Change Container Properties" - bl_description = "Change current ontainer properties" - bl_icon = "FILE_TICK" - bl_options = {'UNDO'} - - scene_scale: bpy.props.FloatProperty(name="Scene Scale", - soft_min=0.0001, soft_max=10.0, - min=1e-6, max=1e6, - default=0.01) # type: ignore - - step_size: bpy.props.FloatProperty(name="Step Size", - soft_min=0.1, soft_max=100.0, - min=0.1, max=1e2, - default=1) # type: ignore - - def execute(self, context): - container_obj = get_container_obj(context.object) - - if container_obj is None: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - container_obj.scale[0] = self.scene_scale - container_obj.scale[1] = self.scene_scale - container_obj.scale[2] = self.scene_scale - container_obj["scene_scale"] = self.scene_scale - container_obj["step_size"] = self.step_size - - for layer_obj in get_container_layer_objs(container_obj): - layer_obj.data.render.space = 'WORLD' - layer_obj.data.render.step_size = self.scene_scale * self.step_size - - return {'FINISHED'} - - def invoke(self, context, event): - container_obj = get_container_obj(context.object) - - if container_obj is None: - return self.execute(context) - else: - self.scene_scale = container_obj.get("scene_scale") or 0.01 - self.step_size = container_obj.get("step_size") or 1 - context.window_manager.invoke_props_dialog(self) - return {'RUNNING_MODAL'} - - -def get_container_layers(context, layer_filter=None): - def _layer_filter(layer_obj, context): - return True - - layer_filter = layer_filter or _layer_filter - container_obj = context.object - layer_objs = get_container_layer_objs(container_obj) - return [obj for obj in layer_objs if layer_filter(obj, context)] - - -class SaveContainerLayersCache(bpy.types.Operator, SaveLayersCache): - bl_idname = "bioxelnodes.save_container_layers_cache" - bl_label = "Save Container Layers' Cache" - bl_description = "Save all current container layers' cache to directory." - bl_icon = "FILE_TICK" - - success_msg = "Successfully saved all container layers." - - def get_layers(self, context): - def is_not_missing(layer_obj, context): - return not is_missing_layer(layer_obj) - return get_container_layers(context, is_not_missing) - - -class RemoveContainerMissingLayers(bpy.types.Operator, RemoveLayers): - bl_idname = "bioxelnodes.remove_container_missing_layers" - bl_label = "Remove Container Missing Layers" - bl_description = "Remove all current container missing layers" - bl_icon = "BRUSH_DATA" - - success_msg = "Successfully removed all container missing layers." - - def get_layers(self, context): - def is_missing(layer_obj, context): - return is_missing_layer(layer_obj) - return get_container_layers(context, is_missing) - - def invoke(self, context, event): - context.window_manager.invoke_confirm(self, - event, - message=f"Are you sure to remove all **Missing** layers?") - return {'RUNNING_MODAL'} diff --git a/src/bioxelnodes/operators/io.py b/src/bioxelnodes/operators/io.py index 0fce9cf..96e1b5f 100644 --- a/src/bioxelnodes/operators/io.py +++ b/src/bioxelnodes/operators/io.py @@ -1,145 +1,168 @@ import math -import bpy import shutil import threading -import numpy as np from pathlib import Path - -from ..exceptions import CancelledByUser -from ..props import BIOXELNODES_Series -from ..bioxelutils.common import (get_all_layer_objs, get_container_obj, - get_layer_obj, is_incompatible) -from ..bioxelutils.container import (Container, - add_layers, - container_to_obj) -from ..bioxel.layer import Layer -from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS, - get_ext, parse_volumetric_data) - -from ..utils import (get_cache_dir, progress_update, progress_bar, - select_object) - -# 3rd-party +import bpy +import numpy as np import SimpleITK as sitk import transforms3d +# KeyboardInterrupt replaced with built-in KeyboardInterrupt +from ..props import BIOXEL_Series +from ..utils import get_layer_obj, wrapped_label + +from ..bioxel.layer import Layer +from ..bioxel.parse import DICOM_EXTS, SUPPORT_EXTS, get_ext, parse_volumetric_data + +from ..utils import get_cache_dir, progress_update, progress_bar +from ..layer import get_layer_caches, save_layers_to_json + def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple): - shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]), - int(orig_shape[1] / bioxel_size * orig_spacing[1]), - int(orig_shape[2] / bioxel_size * orig_spacing[2])) + shape = ( + int(orig_shape[0] / bioxel_size * orig_spacing[0]), + int(orig_shape[1] / bioxel_size * orig_spacing[1]), + int(orig_shape[2] / bioxel_size * orig_spacing[2]), + ) - return (shape[0] if shape[0] > 0 else 1, - shape[1] if shape[1] > 0 else 1, - shape[2] if shape[2] > 0 else 1) + return ( + shape[0] if shape[0] > 0 else 1, + shape[1] if shape[1] > 0 else 1, + shape[2] if shape[2] > 0 else 1, + ) def get_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0): - size = (float(shape[0] * bioxel_size * scale), - float(shape[1] * bioxel_size * scale), - float(shape[2] * bioxel_size * scale)) + size = ( + float(shape[0] * bioxel_size * scale), + float(shape[1] * bioxel_size * scale), + float(shape[2] * bioxel_size * scale), + ) return size """ -ImportVolumetricData - -> ParseVolumetricData -> ImportVolumetricDataDialog -FH_ImportVolumetricData - - start import parse data execute import +ImportData -> ParseVolumetricData -> ImportDataDialog + start import parse data execute import """ -class ImportVolumetricData(): - bl_options = {'UNDO'} - +class ImportDataBase: + bl_options = {"UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore + read_as = None - read_as = "SCALAR" + def draw(self, context): + """ + Display helpful information in the file browser's properties region + when the file selector is open for this operator. + """ + layout = self.layout + exts_list = sorted(SUPPORT_EXTS) + layout.label(text="Supported formats:") + chunk_size = 6 # adjust per-line count to taste + for i in range(0, len(exts_list), chunk_size): + layout.label(text=" ".join(exts_list[i : i + chunk_size])) + + layout.separator() + layout.label(text="Tips:") + wrapped_label( + layout, "- Use 'Import Data' button in Bioxel panel to import data" + ) + wrapped_label( + layout, "- Use 'Import as ...' to choose data type (Scalar/Label/Color)" + ) + wrapped_label( + layout, "- If the file contains multiple series, select series after open" + ) def execute(self, context): data_path = Path(self.filepath).resolve() ext = get_ext(data_path) if ext not in SUPPORT_EXTS: self.report({"WARNING"}, "Not supported format.") - return {'CANCELLED'} + return {"CANCELLED"} - bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT', - filepath=self.filepath, - skip_read_as=True, - read_as=self.read_as) + if self.read_as is None: + bpy.ops.bioxel.parse_volumetric_data( + "INVOKE_DEFAULT", filepath=self.filepath, skip_read_as=False + ) + else: - return {'FINISHED'} + bpy.ops.bioxel.parse_volumetric_data( + "INVOKE_DEFAULT", + filepath=self.filepath, + skip_read_as=True, + read_as=self.read_as, + ) + + return {"FINISHED"} def invoke(self, context, event): context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} -class ImportAsScalar(bpy.types.Operator, ImportVolumetricData): - bl_idname = "bioxelnodes.import_as_scalar" +class ImportData(bpy.types.Operator, ImportDataBase): + bl_idname = "bioxel.import_data" + bl_label = "Import Data" + bl_description = "Import Volumetric Data to Layer" + bl_icon = "EVENT_S" + + +class ImportAsScalar(bpy.types.Operator, ImportDataBase): + bl_idname = "bioxel.import_as_scalar" bl_label = "Import as Scalar" bl_description = "Import Volumetric Data to Container as Scalar" bl_icon = "EVENT_S" read_as = "SCALAR" -class ImportAsLabel(bpy.types.Operator, ImportVolumetricData): - bl_idname = "bioxelnodes.import_as_label" +class ImportAsLabel(bpy.types.Operator, ImportDataBase): + bl_idname = "bioxel.import_as_label" bl_label = "Import as Label" bl_description = "Import Volumetric Data to Container as Label" bl_icon = "EVENT_L" read_as = "LABEL" -class ImportAsColor(bpy.types.Operator, ImportVolumetricData): - bl_idname = "bioxelnodes.import_as_color" +class ImportAsColor(bpy.types.Operator, ImportDataBase): + bl_idname = "bioxel.import_as_color" bl_label = "Import as Color" bl_description = "Import Volumetric Data to Container as Color" bl_icon = "EVENT_C" read_as = "COLOR" -class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler): - bl_idname = "BIOXELNODES_FH_ImportVolumetricData" +class BIOXELNODES_FH_ImportData(bpy.types.FileHandler): + bl_idname = "BIOXELNODES_FH_ImportData" bl_label = "File handler for dicom import" - bl_import_operator = "bioxelnodes.parse_volumetric_data" + bl_import_operator = "bioxel.parse_volumetric_data" bl_file_extensions = ";".join(SUPPORT_EXTS) @classmethod def poll_drop(cls, context): - if not context.area: - return False - - if context.area.type == 'VIEW_3D': - return True - elif context.area.type == 'NODE_EDITOR': - container_obj = get_container_obj(context.object) - return container_obj is not None - else: - return False + return ( + bpy.context.area.type == "NODE_EDITOR" + and bpy.context.space_data.node_tree.bl_idname == "GeometryNodeTree" + ) def get_series_ids(self, context): items = [] for index, series_id in enumerate(self.series_ids): - items.append(( - series_id.id, - series_id.label, - "", - index - )) + items.append((series_id.id, series_id.label, "", index)) return items class ParseVolumetricData(bpy.types.Operator): - bl_idname = "bioxelnodes.parse_volumetric_data" + bl_idname = "bioxel.parse_volumetric_data" bl_label = "Import Volumetric Data (BioxelNodes)" bl_description = "Import Volumetric Data as Layer" - bl_options = {'UNDO'} + bl_options = {"UNDO"} meta = None label_count = 0 @@ -147,62 +170,53 @@ class ParseVolumetricData(bpy.types.Operator): thread = None _timer = None - progress: bpy.props.FloatProperty(name="Progress", - options={"SKIP_SAVE"}, - default=1) # type: ignore + progress: bpy.props.FloatProperty( + name="Progress", options={"SKIP_SAVE"}, default=1 + ) # type: ignore filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore - skip_read_as: bpy.props.BoolProperty(name="Skip Read As", - default=False, - options={"SKIP_SAVE"}) # type: ignore + skip_read_as: bpy.props.BoolProperty( + name="Skip Read As", default=False, options={"SKIP_SAVE"} + ) # type: ignore - skip_series_select: bpy.props.BoolProperty(name="Skip Sries Select", - default=True, - options={"SKIP_SAVE"}) # type: ignore + skip_series_select: bpy.props.BoolProperty( + name="Skip Sries Select", default=True, options={"SKIP_SAVE"} + ) # type: ignore - read_as: bpy.props.EnumProperty(name="Read as", - default="SCALAR", - items=[("SCALAR", "Scalar", ""), - ("LABEL", "Label", ""), - ("COLOR", "Color", "")]) # type: ignore + read_as: bpy.props.EnumProperty( + name="Read as", + default="SCALAR", + items=[ + ("SCALAR", "Scalar", ""), + ("LABEL", "Label", ""), + ("COLOR", "Color", ""), + ], + ) # type: ignore - series_id: bpy.props.EnumProperty(name="Select Series", - items=get_series_ids) # type: ignore + series_id: bpy.props.EnumProperty( + name="Select Series", items=get_series_ids + ) # type: ignore - series_ids: bpy.props.CollectionProperty( - type=BIOXELNODES_Series) # type: ignore + series_ids: bpy.props.CollectionProperty(type=BIOXEL_Series) # type: ignore def execute(self, context): - if is_incompatible(): - self.report({"ERROR"}, - "Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version") - return {'CANCELLED'} - - if not self.filepath: - self.report({"WARNING"}, "No file selected.") - return {'CANCELLED'} - - data_path = Path(self.filepath).resolve() - ext = get_ext(data_path) - if ext not in SUPPORT_EXTS: - self.report({"WARNING"}, "Not supported format.") - return {'CANCELLED'} - print("Collecting Meta Data...") def parse_volumetric_data_func(self, context, cancel): def progress_callback(factor, text): if cancel(): - raise CancelledByUser + raise KeyboardInterrupt("Cancelled by user") progress_update(context, factor, text) try: series_id = self.series_id if self.series_id != "empty" else "" - data, meta = parse_volumetric_data(data_file=self.filepath, - series_id=series_id, - progress_callback=progress_callback) - except CancelledByUser: + data, meta = parse_volumetric_data( + data_file=self.filepath, + series_id=series_id, + progress_callback=progress_callback, + ) + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -220,38 +234,41 @@ class ParseVolumetricData(bpy.types.Operator): self.has_error = None # Create the thread - self.thread = threading.Thread(target=parse_volumetric_data_func, - args=(self, context, lambda: self.is_cancelled)) + self.thread = threading.Thread( + target=parse_volumetric_data_func, + args=(self, context, lambda: self.is_cancelled), + ) # Start the thread self.thread.start() # Add a timmer for modal - self._timer = context.window_manager.event_timer_add(time_step=0.1, - window=context.window) + self._timer = context.window_manager.event_timer_add( + time_step=0.1, window=context.window + ) # Append progress bar to status bar bpy.types.STATUSBAR_HT_header.append(progress_bar) # Start modal handler context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def modal(self, context, event): # Check if user press 'ESC' - if event.type == 'ESC': + if event.type == "ESC": self.is_cancelled = True progress_update(context, 0.0, "Canceling...") - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} # Check if is the timer time - if event.type != 'TIMER': - return {'PASS_THROUGH'} + if event.type != "TIMER": + return {"PASS_THROUGH"} # Force update status bar bpy.context.workspace.status_text_set_internal(None) # Check if thread is still running if self.thread.is_alive(): - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} # Release the thread self.thread.join() @@ -264,7 +281,7 @@ class ParseVolumetricData(bpy.types.Operator): # Check if thread is cancelled by user if self.is_cancelled: self.report({"WARNING"}, "Canncelled by user.") - return {'CANCELLED'} + return {"CANCELLED"} # Check if thread is cancelled by user if self.has_error: @@ -273,7 +290,7 @@ class ParseVolumetricData(bpy.types.Operator): # Check if has return if self.meta is None: self.report({"ERROR"}, "Some thing went wrong.") - return {'CANCELLED'} + return {"CANCELLED"} # If not canncelled... for key, value in self.meta.items(): @@ -282,14 +299,14 @@ class ParseVolumetricData(bpy.types.Operator): if self.read_as == "LABEL": if self.label_count > 100 or self.dtype.kind not in ["i", "u"]: self.report({"ERROR"}, "Invaild label data.") - return {'CANCELLED'} + return {"CANCELLED"} if self.label_count == 0: self.report({"ERROR"}, "Get no label.") - return {'CANCELLED'} + return {"CANCELLED"} - orig_shape = self.meta['xyz_shape'] - orig_spacing = self.meta['spacing'] + orig_shape = self.meta["xyz_shape"] + orig_spacing = self.meta["spacing"] min_log10 = math.floor(math.log10(min(*orig_spacing))) max_log10 = math.floor(math.log10(max(*orig_spacing))) @@ -297,82 +314,75 @@ class ParseVolumetricData(bpy.types.Operator): # max_space = max(*orig_spacing) if orig_spacing[2] == 1 and min_log10 < -1: - orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-2), - orig_spacing[1] * math.pow(10, -min_log10-2), - 1) + orig_spacing = ( + orig_spacing[0] * math.pow(10, -min_log10 - 2), + orig_spacing[1] * math.pow(10, -min_log10 - 2), + 1, + ) elif min_log10 > 0: - orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-1), - orig_spacing[1] * math.pow(10, -min_log10-1), - orig_spacing[2] * math.pow(10, -min_log10-1)) + orig_spacing = ( + orig_spacing[0] * math.pow(10, -min_log10 - 1), + orig_spacing[1] * math.pow(10, -min_log10 - 1), + orig_spacing[2] * math.pow(10, -min_log10 - 1), + ) elif max_log10 < 0: - orig_spacing = (orig_spacing[0] * math.pow(10, -max_log10-1), - orig_spacing[1] * math.pow(10, -max_log10-1), - orig_spacing[2] * math.pow(10, -max_log10-1)) + orig_spacing = ( + orig_spacing[0] * math.pow(10, -max_log10 - 1), + orig_spacing[1] * math.pow(10, -max_log10 - 1), + orig_spacing[2] * math.pow(10, -max_log10 - 1), + ) bioxel_size = max(min(*orig_spacing), 1.0) - layer_shape = get_layer_shape(bioxel_size, - orig_shape, - orig_spacing) - layer_size = get_layer_size(layer_shape, - bioxel_size, - 0.01) + layer_shape = get_layer_shape(bioxel_size, orig_shape, orig_spacing) + layer_size = get_layer_size(layer_shape, bioxel_size, 0.01) min_log10 = math.floor(math.log10(min(*layer_size))) max_log10 = math.floor(math.log10(max(*layer_size))) if min_log10 > 0: - scene_scale = math.pow(10, -min_log10-2) + scene_scale = math.pow(10, -min_log10 - 2) elif max_log10 < 0: - scene_scale = math.pow(10, -max_log10-2) + scene_scale = math.pow(10, -max_log10 - 2) else: scene_scale = 0.01 - if context.area.type == "NODE_EDITOR": - container_obj = context.object - container_name = container_obj.name - container_obj_name = container_name - else: - container_name = self.meta['name'] - container_obj_name = "" - series_id = self.series_id if self.series_id != "empty" else "" - bpy.ops.bioxelnodes.import_volumetric_data_dialog( - 'INVOKE_DEFAULT', + bpy.ops.bioxel.import_volumetric_data_dialog( + "INVOKE_DEFAULT", filepath=self.filepath, - container_name=container_name, - layer_name=self.meta['description'], + layer_name=self.meta["description"], orig_shape=orig_shape, orig_spacing=orig_spacing, bioxel_size=bioxel_size, series_id=series_id, - frame_count=self.meta['frame_count'], - channel_count=self.meta['channel_count'], - container_obj_name=container_obj_name, + frame_count=self.meta["frame_count"], + channel_count=self.meta["channel_count"], read_as=self.read_as, label_count=self.label_count, - scene_scale=scene_scale + scene_scale=scene_scale, ) self.report({"INFO"}, "Successfully Readed.") - return {'FINISHED'} + return {"FINISHED"} def invoke(self, context, event): # why not report in execute? # If this operator is executing, a new execute will when pre-one done. - if context.window_manager.bioxelnodes_progress_factor < 1: + if context.window_manager.bioxel_progress_factor < 1: print("A process is executing, please wait for it to finish.") - return {'CANCELLED'} + return {"CANCELLED"} if not self.filepath: - return self.execute(context) + self.report({"WARNING"}, "No file selected.") + return {"CANCELLED"} data_path = Path(self.filepath).resolve() ext = get_ext(data_path) + if ext not in SUPPORT_EXTS: + self.report({"WARNING"}, "Not supported format.") + return {"CANCELLED"} - if context.area.type == "NODE_EDITOR": - title = f"Add to **{context.object.name}**" - else: - title = "Init a Container" + title = f"Add to **{context.object.name}**" # Series Selection if ext in DICOM_EXTS: @@ -386,7 +396,8 @@ class ParseVolumetricData(bpy.types.Operator): for series_id in series_ids: series_files = reader.GetGDCMSeriesFileNames( - str(data_dirpath), series_id) + str(data_dirpath), series_id + ) single = sitk.ImageFileReader() single.SetFileName(series_files[0]) single.LoadPrivateTagsOn() @@ -395,10 +406,12 @@ class ParseVolumetricData(bpy.types.Operator): def get_meta(key): try: stirng = single.GetMetaData(key).removesuffix(" ") - stirng.encode('utf-8') - if stirng in ["No study description", - "No series description", - ""]: + stirng.encode("utf-8") + if stirng in [ + "No study description", + "No series description", + "", + ]: return "Unknown" else: return stirng @@ -421,8 +434,10 @@ class ParseVolumetricData(bpy.types.Operator): if series_id == "": series_id = "empty" - label = "{:<20} {:>1}".format(f"{study_description}>{series_description}({series_modality})", - f"({size_x}x{size_y})x{count}") + label = "{:<20} {:>1}".format( + f"{study_description}>{series_description}({series_modality})", + f"({size_x}x{size_y})x{count}", + ) series_items[series_id] = label @@ -433,32 +448,27 @@ class ParseVolumetricData(bpy.types.Operator): if len(series_items.keys()) > 1: self.skip_series_select = False - context.window_manager.invoke_props_dialog(self, - width=400, - title=title) - return {'RUNNING_MODAL'} + context.window_manager.invoke_props_dialog(self, width=400, title=title) + return {"RUNNING_MODAL"} elif len(series_items.keys()) == 1: self.series_id = list(series_items.keys())[0] else: self.report({"ERROR"}, "Get no vaild series.") - return {'CANCELLED'} + return {"CANCELLED"} if self.skip_read_as: return self.execute(context) else: - context.window_manager.invoke_props_dialog(self, - width=400, - title=title) - return {'RUNNING_MODAL'} + context.window_manager.invoke_props_dialog(self, width=400, title=title) + return {"RUNNING_MODAL"} def draw(self, context): layout = self.layout if not self.skip_read_as: - layout.label( - text='What kind is the data read as?') + layout.label(text="What kind is the data read as?") layout.prop(self, "read_as") if not self.skip_series_select: - layout.label(text='Which series to import?') + layout.label(text="Which series to import?") layout.prop(self, "series_id") @@ -477,66 +487,63 @@ def get_frame_sources(self, context): return items -class ImportVolumetricDataDialog(bpy.types.Operator): - bl_idname = "bioxelnodes.import_volumetric_data_dialog" +class ImportDataDialog(bpy.types.Operator): + bl_idname = "bioxel.import_volumetric_data_dialog" bl_label = "Import Volumetric Data" bl_description = "Import Volumetric Data as Layer" - bl_options = {'UNDO'} + bl_options = {"UNDO"} layers = None thread = None _timer = None filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore - - container_name: bpy.props.StringProperty( - name="Container Name") # type: ignore - - layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore - - series_id: bpy.props.StringProperty() # type: ignore - - container_obj_name: bpy.props.StringProperty() # type: ignore - + layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore + series_id: bpy.props.StringProperty() # type: ignore frame_count: bpy.props.IntProperty() # type: ignore - channel_count: bpy.props.IntProperty() # type: ignore - label_count: bpy.props.IntProperty() # type: ignore - - smooth: bpy.props.IntProperty(name="Smooth Size (Larger takes longer time)", - default=0) # type: ignore - - read_as: bpy.props.EnumProperty(name="Read as", - default="SCALAR", - items=[("SCALAR", "Scalar", ""), - ("LABEL", "Label", ""), - ("COLOR", "Color", "")]) # type: ignore - - bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)", - soft_min=0.1, soft_max=10.0, - min=0.1, max=1e2, - default=1) # type: ignore - - orig_spacing: bpy.props.FloatVectorProperty(name="Original Spacing", - default=(1, 1, 1)) # type: ignore - - orig_shape: bpy.props.IntVectorProperty(name="Original Shape", - default=(100, 100, 100)) # type: ignore - - scene_scale: bpy.props.FloatProperty(name="Scene Scale (Bioxel Unit pre Blender Unit)", - soft_min=0.0001, soft_max=10.0, - min=1e-6, max=1e6, - default=0.01) # type: ignore - - remap: bpy.props.BoolProperty(name="Remap to 0~1", - default=False) # type: ignore - - split_channel: bpy.props.BoolProperty(name="Split Channels", - default=False) # type: ignore - - frame_source: bpy.props.EnumProperty(name="Frame From", - items=get_frame_sources) # type: ignore + smooth: bpy.props.IntProperty( + name="Smooth Size (Larger takes longer time)", default=0 + ) # type: ignore + read_as: bpy.props.EnumProperty( + name="Read as", + default="SCALAR", + items=[ + ("SCALAR", "Scalar", ""), + ("LABEL", "Label", ""), + ("COLOR", "Color", ""), + ], + ) # type: ignore + bioxel_size: bpy.props.FloatProperty( + name="Bioxel Size (Larger size means small resolution)", + soft_min=0.1, + soft_max=10.0, + min=0.1, + max=1e2, + default=1, + ) # type: ignore + orig_spacing: bpy.props.FloatVectorProperty( + name="Original Spacing", default=(1, 1, 1) + ) # type: ignore + orig_shape: bpy.props.IntVectorProperty( + name="Original Shape", default=(100, 100, 100) + ) # type: ignore + scene_scale: bpy.props.FloatProperty( + name="Scene Scale (Bioxel Unit pre Blender Unit)", + soft_min=0.0001, + soft_max=10.0, + min=1e-6, + max=1e6, + default=0.01, + ) # type: ignore + remap: bpy.props.BoolProperty(name="Remap to 0~1", default=False) # type: ignore + split_channel: bpy.props.BoolProperty( + name="Split Channels", default=False + ) # type: ignore + frame_source: bpy.props.EnumProperty( + name="Frame From", items=get_frame_sources + ) # type: ignore def execute(self, context): def import_volumetric_data_func(self, context, cancel): @@ -544,14 +551,32 @@ class ImportVolumetricDataDialog(bpy.types.Operator): def progress_callback(factor, text): if cancel(): - raise CancelledByUser - progress_update(context, factor*0.2, text) + raise KeyboardInterrupt("Cancelled by user") + progress_update(context, factor * 0.2, text) + + def progress_callback_factory(layer_name, progress, progress_step): + def progress_callback(frame, total): + if cancel(): + raise KeyboardInterrupt("Cancelled by user") + sub_progress_step = progress_step / total + sub_progress = progress + frame * sub_progress_step + progress_update( + context, + sub_progress, + f"Processing {layer_name} Frame {frame+1}...", + ) + print(f"Processing {layer_name} Frame {frame+1}...") + + return progress_callback try: - data, meta = parse_volumetric_data(data_file=self.filepath, - series_id=self.series_id, - progress_callback=progress_callback) - except CancelledByUser: + data, meta = parse_volumetric_data( + data_file=self.filepath, + series_id=self.series_id, + progress_callback=progress_callback, + ) + + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -560,12 +585,12 @@ class ImportVolumetricDataDialog(bpy.types.Operator): if cancel(): return - shape = get_layer_shape(self.bioxel_size, - self.orig_shape, - self.orig_spacing) + shape = get_layer_shape( + self.bioxel_size, self.orig_shape, self.orig_spacing + ) mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size) - affine = np.dot(meta['affine'], mat_scale) + affine = np.dot(meta["affine"], mat_scale) kind = self.read_as.lower() if cancel(): @@ -593,51 +618,39 @@ class ImportVolumetricDataDialog(bpy.types.Operator): # channel as frame data = data.transpose(4, 1, 2, 3, 0) - def progress_callback_factory(layer_name, progress, progress_step): - def progress_callback(frame, total): - if cancel(): - raise CancelledByUser - sub_progress_step = progress_step/total - sub_progress = progress + frame * sub_progress_step - progress_update(context, sub_progress, - f"Processing {layer_name} Frame {frame+1}...") - print(f"Processing {layer_name} Frame {frame+1}...") - return progress_callback - layers = [] if kind == "label": name = self.layer_name or "Label" data = data.astype(int) label_count = int(np.max(data)) - progress_step = 0.7/label_count + progress_step = 0.7 / label_count for i in range(label_count): if cancel(): return name_i = f"{name}_{i+1}" - progress = 0.2+i*progress_step - progress_update(context, progress, - f"Processing {name_i}...") + progress = 0.2 + i * progress_step + progress_update(context, progress, f"Processing {name_i}...") - progress_callback = progress_callback_factory(name_i, - progress, - progress_step) - label_data = data == np.full_like(data, i+1) + progress_callback = progress_callback_factory( + name_i, progress, progress_step + ) + label_data = data == np.full_like(data, i + 1) # label_data = label_data.astype(np.float32) try: - layer = Layer(data=label_data, - name=name_i, - kind=kind) + layer = Layer(data=label_data, name=name_i, kind=kind) - layer.resize(shape=shape, - smooth=self.smooth, - progress_callback=progress_callback) + layer.resize( + shape=shape, + smooth=self.smooth, + progress_callback=progress_callback, + ) layer.affine = affine layers.append(layer) - except CancelledByUser: + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -645,9 +658,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator): if kind == "color": if np.issubdtype(np.uint8, data.dtype): - data = np.multiply(data, 1.0 / 256, - dtype=np.float32) - elif data.dtype.kind in ['u', 'i']: + data = np.multiply(data, 1.0 / 256, dtype=np.float32) + elif data.dtype.kind in ["u", "i"]: # Convert the normalized array to float dtype data = data.astype(np.float32) @@ -681,24 +693,18 @@ class ImportVolumetricDataDialog(bpy.types.Operator): if cancel(): return - progress_update(context, 0.2, - f"Processing {name}...") - progress_callback = progress_callback_factory(name, - 0.2, - 0.7) + progress_update(context, 0.2, f"Processing {name}...") + progress_callback = progress_callback_factory(name, 0.2, 0.7) try: - layer = Layer(data=data, - name=name, - kind=kind) + layer = Layer(data=data, name=name, kind=kind) - layer.resize(shape=shape, - progress_callback=progress_callback) + layer.resize(shape=shape, progress_callback=progress_callback) layer.affine = affine layers.append(layer) - except CancelledByUser: + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -722,31 +728,31 @@ class ImportVolumetricDataDialog(bpy.types.Operator): data = np.zeros_like(data, dtype=np.float32) if self.split_channel: - progress_step = 0.7/self.channel_count + progress_step = 0.7 / self.channel_count for i in range(self.channel_count): if cancel(): return name_i = f"{name}_{i+1}" - progress = 0.2 + i*progress_step - progress_update(context, progress, - f"Processing {name_i}...") - progress_callback = progress_callback_factory(name_i, - progress, - progress_step) + progress = 0.2 + i * progress_step + progress_update(context, progress, f"Processing {name_i}...") + progress_callback = progress_callback_factory( + name_i, progress, progress_step + ) try: - layer = Layer(data=data[:, :, :, :, i:i+1], - name=name_i, - kind=kind) + layer = Layer( + data=data[:, :, :, :, i : i + 1], name=name_i, kind=kind + ) - layer.resize(shape=shape, - progress_callback=progress_callback) + layer.resize( + shape=shape, progress_callback=progress_callback + ) layer.affine = affine layers.append(layer) - except CancelledByUser: + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -755,24 +761,18 @@ class ImportVolumetricDataDialog(bpy.types.Operator): if cancel(): return - progress_update(context, 0.2, - f"Processing {name}...") - progress_callback = progress_callback_factory(name, - 0.2, - 0.7) + progress_update(context, 0.2, f"Processing {name}...") + progress_callback = progress_callback_factory(name, 0.2, 0.7) try: - layer = Layer(data=data, - name=name, - kind=kind) + layer = Layer(data=data, name=name, kind=kind) - layer.resize(shape=shape, - progress_callback=progress_callback) + layer.resize(shape=shape, progress_callback=progress_callback) layer.affine = affine layers.append(layer) - except CancelledByUser: + except KeyboardInterrupt: return except Exception as e: self.has_error = e @@ -787,29 +787,32 @@ class ImportVolumetricDataDialog(bpy.types.Operator): self.is_cancelled = False self.has_error = None - self.thread = threading.Thread(target=import_volumetric_data_func, - args=(self, context, lambda: self.is_cancelled)) + self.thread = threading.Thread( + target=import_volumetric_data_func, + args=(self, context, lambda: self.is_cancelled), + ) self.thread.start() - self._timer = context.window_manager.event_timer_add(time_step=0.1, - window=context.window) + self._timer = context.window_manager.event_timer_add( + time_step=0.1, window=context.window + ) bpy.types.STATUSBAR_HT_header.append(progress_bar) context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def modal(self, context, event): - if event.type == 'ESC': + if event.type == "ESC": self.is_cancelled = True progress_update(context, 0.0, "Canceling...") - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} - if event.type != 'TIMER': - return {'PASS_THROUGH'} + if event.type != "TIMER": + return {"PASS_THROUGH"} bpy.context.workspace.status_text_set_internal(None) if self.thread.is_alive(): - return {'PASS_THROUGH'} + return {"PASS_THROUGH"} self.thread.join() context.window_manager.event_timer_remove(self._timer) @@ -818,7 +821,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator): if self.is_cancelled: self.report({"WARNING"}, "Canncelled by user.") - return {'CANCELLED'} + return {"CANCELLED"} # Check if thread is cancelled by user if self.has_error: @@ -827,69 +830,30 @@ class ImportVolumetricDataDialog(bpy.types.Operator): # Check if has return if self.layers is None: self.report({"ERROR"}, "Some thing went wrong.") - return {'CANCELLED'} + return {"CANCELLED"} - is_first_import = len(get_all_layer_objs()) == 0 + is_first_import = len(get_layer_caches()) == 0 + ids = save_layers_to_json(self.layers, cache_dir=get_cache_dir() / "layers") - if self.container_obj_name: - container_obj = bpy.data.objects.get(self.container_obj_name) - if container_obj is None: - raise Exception("Could not find target container") - - container_obj = add_layers(self.layers, - container_obj=container_obj, - cache_dir=get_cache_dir()) - else: - name = self.container_name or "Container" - container = Container(name=name, - layers=self.layers) - - step_size = container.layers[0].bioxel_size[0]*10 - container_obj = container_to_obj(container, - scene_scale=self.scene_scale, - step_size=step_size, - cache_dir=get_cache_dir()) - - select_object(container_obj) + setattr(context.window_manager, "bioxel_layer_library", ids[-1]) # Change render setting for better result if is_first_import: - try: - bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT', - preset="balance") - bpy.ops.bioxelnodes.add_eevee_env('EXEC_DEFAULT') - bpy.context.scene.eevee.use_taa_reprojection = False - bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT' - - view_3d = None - if context.area.type == 'VIEW_3D': - view_3d = context.area - else: - for area in context.screen.areas: - if area.type == 'VIEW_3D': - view_3d = area - break - if view_3d: - view_3d.spaces[0].shading.type = 'RENDERED' - except: - self.report({"WARNING"}, "Fail to change render setting.") + bpy.ops.bioxel.render_setting_preset("EXEC_DEFAULT", preset="balance") self.report({"INFO"}, "Successfully Imported") - return {'FINISHED'} + return {"FINISHED"} def invoke(self, context, event): layer_kind = self.read_as.capitalize() - title = f"Add to **{self.container_obj_name}**, As {layer_kind}" \ - if self.container_obj_name != "" else f"Init a Container, As {layer_kind}" - context.window_manager.invoke_props_dialog(self, - width=500, - title=title) - return {'RUNNING_MODAL'} + title = f"Add to **{context.object.name}**, As {layer_kind}" + context.window_manager.invoke_props_dialog(self, width=500, title=title) + return {"RUNNING_MODAL"} def draw(self, context): - layer_shape = get_layer_shape(self.bioxel_size, - self.orig_shape, - self.orig_spacing) + layer_shape = get_layer_shape( + self.bioxel_size, self.orig_shape, self.orig_spacing + ) orig_shape = tuple(self.orig_shape) @@ -925,24 +889,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator): channel_count = 3 bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2] - orig_shape_text = f"[{self.frame_count}, ({orig_shape[0]},{orig_shape[1]},{orig_shape[2]}), {self.channel_count}]" - layer_shape_text = f"{layer_count} x [{frame_count}, ({layer_shape[0]},{layer_shape[1]},{layer_shape[2]}), {channel_count}]" + orig_shape_text = f"[{self.frame_count}, {orig_shape[0]},{orig_shape[1]},{orig_shape[2]}, {self.channel_count}]" + layer_shape_text = f"{layer_count} x [{frame_count}, {layer_shape[0]},{layer_shape[1]},{layer_shape[2]}, {channel_count}]" if bioxel_count > 100000000: layer_shape_text += "**TOO LARGE!**" - layer_size = get_layer_size(layer_shape, - self.bioxel_size, - self.scene_scale) - layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m" - layout = self.layout - if self.container_obj_name == "": - panel = layout.box() - panel.prop(self, "container_name") - panel.prop(self, "scene_scale") - panel.label(text=layer_size_text) - panel = layout.box() panel.prop(self, "layer_name") panel.prop(self, "bioxel_size") @@ -951,26 +904,21 @@ class ImportVolumetricDataDialog(bpy.types.Operator): panel.prop(self, "frame_source") if self.read_as == "SCALAR": - panel.prop(self, "split_channel", - text=f"Split Channel as Multi Layer") + panel.prop(self, "split_channel", text=f"Split Channel as Multi Layer") elif self.read_as == "LABEL": panel.prop(self, "smooth") - panel.label( - text="Dimension Order: [Frame, (X-axis,Y-axis,Z-axis), Channel]") - panel.label( - text=f"Shape from {orig_shape_text} to {layer_shape_text}") + panel.label(text=f"Shape from {orig_shape_text} to {layer_shape_text}") + panel.label(text="Dimension Order: [Frame, X-axis, Y-axis, Z-axis, Channel]") class ExportVolumetricData(bpy.types.Operator): - bl_idname = "bioxelnodes.export_volumetric_data" + bl_idname = "bioxel.export_volumetric_data" bl_label = "Export Layer as VDB" bl_description = "Export Layer as VDB" - bl_options = {'UNDO'} + bl_options = {"UNDO"} - filepath: bpy.props.StringProperty( - subtype="FILE_PATH" - ) # type: ignore + filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore filename_ext = ".vdb" @@ -981,7 +929,7 @@ class ExportVolumetricData(bpy.types.Operator): def invoke(self, context, event): context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} def execute(self, context): layer_obj = get_layer_obj(bpy.context.active_object) @@ -998,4 +946,4 @@ class ExportVolumetricData(bpy.types.Operator): self.report({"INFO"}, f"Successfully exported to {output_path}") - return {'FINISHED'} + return {"FINISHED"} diff --git a/src/bioxelnodes/operators/layer.py b/src/bioxelnodes/operators/layer.py index 5cbf71b..6825012 100644 --- a/src/bioxelnodes/operators/layer.py +++ b/src/bioxelnodes/operators/layer.py @@ -1,619 +1,320 @@ from pathlib import Path +import shutil + import bpy -import numpy as np +from ..node import add_bioxel_node, get_layer_nodes, get_main_node_group +from ..utils import refresh_bioxel_panels +from ..layer import get_layer_caches, set_layer_caches -from ..exceptions import NoContent -from ..bioxel.layer import Layer -from ..bioxelutils.node import add_node_to_graph -from ..bioxelutils.common import (get_container_obj, get_layer_kind, get_layer_label, get_layer_name, - get_layer_prop_value, - get_container_layer_objs, - get_node_type, is_missing_layer, move_node_to_node, set_layer_prop_value) -from ..bioxelutils.layer import layer_to_obj, obj_to_layer -from ..utils import get_cache_dir, copy_to_dir, get_use_link +class RenameLayer(bpy.types.Operator): + """Rename a cached Bioxel layer""" + bl_idname = "bioxel.rename_layer" + bl_label = "Rename Layer" + bl_options = {"REGISTER", "UNDO"} -def get_label_layer_selection(self, context): - items = [("None", "None", "")] - container_obj = get_container_obj(bpy.context.active_object) + cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore + new_name: bpy.props.StringProperty( + name="New name", default="") # type: ignore - for layer_obj in get_container_layer_objs(container_obj): - kind = get_layer_prop_value(layer_obj, "kind") - name = get_layer_prop_value(layer_obj, "name") - if kind == "label": - items.append((layer_obj.name, - name, - "")) - - return items - - -class FetchLayer(bpy.types.Operator): - bl_idname = "bioxelnodes.fetch_layer" - bl_label = "Fetch Layer" - bl_description = "Fetch layer from current container" - bl_icon = "NODE" - bl_options = {'UNDO'} - - layer_obj_name: bpy.props.StringProperty( - options={"HIDDEN"}) # type: ignore - - @classmethod - def description(cls, context, properties): - layer_obj_name = properties.layer_obj_name - layer_obj = bpy.data.objects.get(layer_obj_name) - return "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}" - for prop in ["kind", - "bioxel_size", - "shape", - "frame_count", - "channel_count", - "min", "max"]]) - - @property - def layer_obj(self): - return bpy.data.objects.get(self.layer_obj_name) + def invoke(self, context, event): + caches = get_layer_caches() + entry = next((c for c in caches if str( + c.get("id", "")) == self.cache_id), None) + if entry: + self.new_name = str(entry.get("name", "")) + return context.window_manager.invoke_props_dialog(self) def execute(self, context): - if self.layer_obj == None: - self.report({"WARNING"}, "Get no layer.") - return {'FINISHED'} + if not self.new_name: + self.report({"ERROR"}, "Name cannot be empty") + return {"CANCELLED"} - if is_missing_layer(self.layer_obj): - self.report({"WARNING"}, "Selected layer is lost.") - return {'FINISHED'} + caches = get_layer_caches() + for c in caches: + if str(c.get("id", "")) == self.cache_id: + c["name"] = self.new_name + break - bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT', - node_name="FetchLayer", - node_label="Fetch Layer") - node = bpy.context.active_node + try: + set_layer_caches(caches) + refresh_bioxel_panels(context) + self.report({"INFO"}, "Layer renamed") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, f"Failed to save changes: {e}") + return {"CANCELLED"} - node.inputs[0].default_value = self.layer_obj - node.label = get_layer_label(self.layer_obj) +class DeleteLayer(bpy.types.Operator): + """Delete a saved Bioxel layer""" + + bl_idname = "bioxel.delete_layer" + bl_label = "Delete Layer" + bl_options = {"REGISTER", "UNDO"} + + cache_id: bpy.props.StringProperty() # type: ignore + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + if not self.cache_id: + self.report({"ERROR"}, "Missing cache id") + return {"CANCELLED"} + + caches = get_layer_caches() + new_list = [c for c in caches if str(c.get("id", "")) != self.cache_id] + removed = next( + (c for c in caches if str(c.get("id", "")) == self.cache_id), None + ) + + try: + set_layer_caches(new_list) + refresh_bioxel_panels(context) + except Exception as e: + self.report({"ERROR"}, f"Failed to update layer list: {e}") + return {"CANCELLED"} + + self.report({"INFO"}, "Layer removed") return {"FINISHED"} -def get_selected_layers(context, layer_filter=None): - def _layer_filter(layer_obj, context): - return True +# 添加图层到节点图的操作器 +class AddLayerNode(bpy.types.Operator): + """将选中的图层添加到几何节点图""" - layer_filter = layer_filter or _layer_filter - select_objs = [] - # node_group = context.space_data.edit_tree - for node in context.selected_nodes: - if get_node_type(node) == "BioxelNodes_FetchLayer": - layer_obj = node.inputs[0].default_value - if layer_obj is not None: - if layer_filter(layer_obj, context): - select_objs.append(layer_obj) + bl_idname = "bioxel.add_layer_node" + bl_label = "添加图层节点" + bl_options = {"REGISTER", "UNDO"} - return list(set(select_objs)) - - -def get_selected_layer(context, layer_filter=None): - layer_objs = get_selected_layers(context, layer_filter) - return layer_objs[0] if len(layer_objs) > 0 else None - - -class OutputLayerOperator(): - new_layer_name: bpy.props.StringProperty(name="New Name", - options={"SKIP_SAVE"}) # type: ignore - - def operate(self, orig_layer: Layer, context): - """do the operation""" - return orig_layer - - def add_layer_node(self, context, layer): - orig_node = context.selected_nodes[0] - layer_obj = layer_to_obj(layer, - container_obj=context.object, - cache_dir=get_cache_dir()) - node_group = context.space_data.edit_tree - fetch_node = add_node_to_graph("FetchLayer", - node_group, - use_link=get_use_link()) - fetch_node.label = get_layer_prop_value(layer_obj, "name") - fetch_node.inputs[0].default_value = layer_obj - move_node_to_node(fetch_node, orig_node, (0, -100)) + cache_id: bpy.props.StringProperty() # type: ignore # 图层ID def execute(self, context): - layer_obj = get_selected_layer(context) - if layer_obj == None: - self.report({"WARNING"}, "Get no layer.") - return {'FINISHED'} + # 获取图层数据 + caches = get_layer_caches() - if is_missing_layer(layer_obj): - self.report({"WARNING"}, "Selected layer is lost.") - return {'FINISHED'} + # 查找目标图层 + entry = next( + (c for c in caches if str(c.get("id", "")) == str(self.cache_id)), None + ) - orig_layer = obj_to_layer(layer_obj) + if not entry: + self.report({"ERROR"}, "Layer not found") + return {"CANCELLED"} + + # 创建并设置节点属性 try: - new_layer = self.operate(orig_layer, context) - except NoContent as e: - self.report({"WARNING"}, e.message) - return {'FINISHED'} + layer_node = add_bioxel_node("O Layer") + except: + self.report({"ERROR"}, "Fail to add layer node") + return {"CANCELLED"} - self.add_layer_node(context, new_layer) - return {'FINISHED'} + layer_node.node_tree.make_local() + layer_node.inputs["Path"].default_value = entry["path"] + layer_node.inputs["Name"].default_value = entry["name"] + layer_node.inputs["Shape"].default_value = entry["shape"] + layer_node.inputs["Min"].default_value = entry["min"] + layer_node.inputs["Max"].default_value = entry["max"] + layer_node.inputs["ID"].default_value = entry["id"] + # 将特定属性隐藏到hidden面板 + hidden_sockets = ["Path", "Shape", "Min", "Max", "ID", "Animation"] -class LayerOperator(): - - def operate(self, layer_obj: bpy.types.Object, context): - """do the operation""" - ... - - def execute(self, context): - layer_obj = get_selected_layer(context) - if layer_obj == None: - self.report({"WARNING"}, "Get no layer.") - return {'FINISHED'} - - if is_missing_layer(layer_obj): - self.report({"WARNING"}, "Selected layer is lost.") - return {'FINISHED'} - - self.operate(layer_obj, context) - - return {'FINISHED'} - - -class RetimeLayer(bpy.types.Operator, LayerOperator): - bl_idname = "bioxelnodes.retime_layer" - bl_label = "Retime Sequence" - bl_description = "Retime layer time sequence" - bl_icon = "TIME" - - frame_duration: bpy.props.IntProperty(name="Frames") # type: ignore - frame_start: bpy.props.IntProperty(name="Start") # type: ignore - frame_offset: bpy.props.IntProperty(name="Offset") # type: ignore - sequence_mode: bpy.props.EnumProperty(name="Mode", - default="REPEAT", - items=[("CLIP", "Clip", ""), - ("EXTEND", "Extend", ""), - ("REPEAT", "Repeat", ""), - ("PING_PONG", "Ping-Pong", "")]) # type: ignore - - def operate(self, layer_obj, context): - layer_obj.data.frame_duration = self.frame_duration - layer_obj.data.frame_start = self.frame_start - layer_obj.data.frame_offset = self.frame_offset - layer_obj.data.sequence_mode = self.sequence_mode - - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - self.frame_duration = layer_obj.data.frame_duration - self.frame_start = layer_obj.data.frame_start - self.frame_offset = layer_obj.data.frame_offset - self.sequence_mode = layer_obj.data.sequence_mode - name = get_layer_label(layer_obj) - context.window_manager.invoke_props_dialog(self, - title=f"Retime {name}") - return {'RUNNING_MODAL'} + if entry["frame_count"] > 1: + layer_node.inputs["Frame Count"].default_value = entry["frame_count"] + layer_node.inputs["Animation"].default_value = True else: - return self.execute(context) + hidden_sockets = hidden_sockets + \ + ["Frame Count", "Frame Offset", "Cycle"] + + # 将特定属性隐藏到hidden面板 + for socket_name in hidden_sockets: + if socket_name in layer_node.inputs: + layer_node.inputs[socket_name].hide = True + + for socket_name in layer_node.outputs.keys(): + if socket_name != entry["kind"].capitalize(): + layer_node.outputs[socket_name].hide = True + + self.report({"INFO"}, f"Successfully created node: {entry['name']}") + return {"FINISHED"} -class RelocateLayer(bpy.types.Operator): - bl_idname = "bioxelnodes.relocate_layer" - bl_label = "Relocate Layer Cache" - bl_description = "Relocate layer cache" - bl_icon = "FILE" +class RelocatePath(bpy.types.Operator): + """Locate and update the missing layer folder, and update all related nodes""" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore + bl_idname = "bioxel.find_layer_folder" + bl_label = "Relocate" + bl_options = {"REGISTER", "UNDO"} - def execute(self, context): - layer_obj = get_selected_layer(context) - if layer_obj == None: - self.report({"WARNING"}, "Get no layer.") - return {'FINISHED'} + cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore - self.operate(layer_obj, context) - - return {'FINISHED'} - - def operate(self, layer_obj, context): - layer_obj.data.filepath = self.filepath - - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - else: - return self.execute(context) - - -class RenameLayer(bpy.types.Operator, LayerOperator): - bl_idname = "bioxelnodes.rename_layer" - bl_label = "Rename Layer" - bl_description = "Rename layer" - bl_icon = "FILE_FONT" - - new_name: bpy.props.StringProperty(name="New Name", - options={"SKIP_SAVE"}) # type: ignore - - def operate(self, layer_obj, context): - name = f"{layer_obj.parent.name}_{self.new_name}" - layer_obj.name = name - layer_obj.data.name = name - - set_layer_prop_value(layer_obj, "name", self.new_name) - - node_group = context.space_data.edit_tree - for node in node_group.nodes: - if get_node_type(node) == "BioxelNodes_FetchLayer": - if node.inputs[0].default_value == layer_obj: - node.label = self.new_name - - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - self.new_name = get_layer_name(layer_obj) - name = get_layer_label(layer_obj) - context.window_manager.invoke_props_dialog(self, - title=f"Rename {name}") - return {'RUNNING_MODAL'} - else: - return self.execute(context) - - -class ResampleLayer(bpy.types.Operator, OutputLayerOperator): - bl_idname = "bioxelnodes.resample_layer" - bl_label = "Resample Value" - bl_description = "Resample value" - bl_icon = "ALIASED" - - smooth: bpy.props.IntProperty(name="Smooth Size", - default=0, - soft_min=0, soft_max=5, - options={"SKIP_SAVE"}) # type: ignore - - bioxel_size: bpy.props.FloatProperty( - name="Bioxel Size", - soft_min=0.1, soft_max=10.0, - default=1, - ) # type: ignore - - @staticmethod - def get_new_shape(orig_shape, orig_size, new_size): - return (int(orig_shape[0]*orig_size/new_size), - int(orig_shape[1]*orig_size/new_size), - int(orig_shape[2]*orig_size/new_size)) - - def operate(self, orig_layer: Layer, context): - new_layer = orig_layer.copy() - new_shape = self.get_new_shape(new_layer.shape, - new_layer.bioxel_size[0], - self.bioxel_size) - - new_layer.resize(new_shape, self.smooth) - new_layer.name = self.new_layer_name \ - or f"{orig_layer.name}_R-{self.bioxel_size:.2f}" - return new_layer - - def draw(self, context): - layer_obj = get_selected_layer(context) - orig_shape = get_layer_prop_value(layer_obj, "shape") - orig_size = get_layer_prop_value(layer_obj, "bioxel_size") - new_shape = self.get_new_shape(orig_shape, - orig_size, - self.bioxel_size) - - orig_shape = tuple(orig_shape) - bioxel_count = new_shape[0] * new_shape[1] * new_shape[2] - - layer_shape_text = f"Shape from {str(orig_shape)} to {str(new_shape)}" - if bioxel_count > 100000000: - layer_shape_text += "**TOO LARGE!**" - - layout = self.layout - layout.prop(self, "new_layer_name") - layout.prop(self, "bioxel_size") - layout.prop(self, "smooth") - layout.label(text=layer_shape_text) - - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - name = get_layer_label(layer_obj) - self.bioxel_size = get_layer_prop_value(layer_obj, - "bioxel_size") - context.window_manager.invoke_props_dialog(self, - title=f"Resample {name}") - return {'RUNNING_MODAL'} - else: - return self.execute(context) - - -class SignScalar(bpy.types.Operator, OutputLayerOperator): - bl_idname = "bioxelnodes.sign_scalar" - bl_label = "Sign Value" - bl_description = "Sign value" - bl_icon = "REMOVE" - - def operate(self, orig_layer: Layer, context): - new_layer = orig_layer.copy() - new_layer.data = -orig_layer.data - new_layer.name = self.new_layer_name \ - or f"{orig_layer.name}_Sign" - return new_layer - - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - name = get_layer_label(layer_obj) - context.window_manager.invoke_props_dialog(self, - title=f"Sign {name}") - return {'RUNNING_MODAL'} - else: - return self.execute(context) - - -class FillOperator(OutputLayerOperator): - - fill_value: bpy.props.FloatProperty( - name="Fill Value", - soft_min=0, soft_max=1024.0, - default=0, - ) # type: ignore - - invert: bpy.props.BoolProperty( - name="Invert Aera", + use_relative: bpy.props.BoolProperty( + name="Relative Path", + description="Store the path relative to the current Blender file", default=True, ) # type: ignore - def invoke(self, context, event): - layer_obj = get_selected_layer(context) - if layer_obj: - name = get_layer_label(layer_obj) - context.window_manager.invoke_props_dialog(self, - title=f"Fill {name}") - return {'RUNNING_MODAL'} - else: - return self.execute(context) - - -class FillByThreshold(bpy.types.Operator, FillOperator): - bl_idname = "bioxelnodes.fill_by_threshold" - bl_label = "Fill Value by Threshold" - bl_description = "Fill value by threshold" - bl_icon = "EMPTY_SINGLE_ARROW" - - threshold: bpy.props.FloatProperty( - name="Threshold", - soft_min=0, soft_max=1024, - default=128, + directory: bpy.props.StringProperty( + name="Layer Folder", + description="Select the correct folder for this layer", + subtype="DIR_PATH", ) # type: ignore - def operate(self, orig_layer: Layer, context): - data = np.amax(orig_layer.data, -1) - mask = data <= self.threshold \ - if self.invert else data > self.threshold - - new_layer = orig_layer.copy() - new_layer.fill(self.fill_value, mask, 0) - new_layer.name = self.new_layer_name \ - or f"{orig_layer.name}_F-{self.threshold:.2f}" - return new_layer - - -class FillByRange(bpy.types.Operator, FillOperator): - bl_idname = "bioxelnodes.fill_by_range" - bl_label = "Fill Value by Range" - bl_description = "Fill value by range" - bl_icon = "IPO_CONSTANT" - - from_min: bpy.props.FloatProperty( - name="From Min", - soft_min=0, soft_max=1024, - default=128, - ) # type: ignore - - from_max: bpy.props.FloatProperty( - name="From Max", - soft_min=0, soft_max=1024, - default=256, - ) # type: ignore - - def operate(self, orig_layer: Layer, context): - data = np.amax(orig_layer.data, -1) - mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \ - (data > self.from_min) & (data < self.from_max) - - new_layer = orig_layer.copy() - new_layer.fill(self.fill_value, mask, 0) - new_layer.name = self.new_layer_name \ - or f"{orig_layer.name}_F-{self.from_min:.2f}-{self.from_max:.2f}" - return new_layer - - -class FillByLabel(bpy.types.Operator, FillOperator): - bl_idname = "bioxelnodes.fill_by_label" - bl_label = "Fill Value by Label" - bl_description = "Fill value by label" - bl_icon = "MESH_CAPSULE" - - smooth: bpy.props.IntProperty(name="Smooth Size", - default=0, - soft_min=0, soft_max=5, - options={"SKIP_SAVE"}) # type: ignore - - label_obj_name: bpy.props.EnumProperty(name="Label Layer", - items=get_label_layer_selection) # type: ignore - - def operate(self, orig_layer: Layer, context): - label_obj = bpy.data.objects.get(self.label_obj_name) - if label_obj is None: - raise NoContent("Cannot find any label layer.") - - label_layer = obj_to_layer(label_obj) - label_layer.resize(orig_layer.shape, self.smooth) - mask = np.amax(label_layer.data, -1) - if self.invert: - mask = 1 - mask - - new_layer = orig_layer.copy() - new_layer.fill(self.fill_value, mask, 0) - new_layer.name = self.new_layer_name \ - or f"{orig_layer.name}_F-{label_layer.name}" - return new_layer - - -class CombineLabels(bpy.types.Operator, OutputLayerOperator): - bl_idname = "bioxelnodes.combine_labels" - bl_label = "Combine Labels" - bl_description = "Combine all selected labels" - bl_icon = "MOD_BUILD" - - def execute(self, context): - def layer_filter(layer_obj, context): - return get_layer_kind(layer_obj) == "label" - - label_objs = get_selected_layers(context, layer_filter) - - if len(label_objs) < 2: - self.report({"WARNING"}, "Not enough layers.") - return {'FINISHED'} - - base_obj = label_objs[0] - label_objs = label_objs[1:] - - base_layer = obj_to_layer(base_obj) - new_layer = base_layer.copy() - label_names = [base_layer.name] - - for label_obj in label_objs: - label_layer = obj_to_layer(label_obj) - label_layer.resize(base_layer.shape) - new_layer.data = np.maximum( - new_layer.data, label_layer.data) - label_names.append(label_layer.name) - - new_layer.name = self.new_layer_name \ - or f"C-{'-'.join(label_names)}" - - self.add_layer_node(context, new_layer) - return {'FINISHED'} - - -class BatchLayerOperator(): - success_msg = "Successfully done selected layers." - fail_msg = "fails." - - def execute(self, context): - layer_objs = self.get_layers(context) - - fails = [] - for layer_obj in layer_objs: - try: - self.operate(layer_obj, context) - except: - fails.append(layer_obj) - - if len(fails) == 0: - self.report({"INFO"}, self.success_msg) - else: - self.report( - {"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} {self.fail_msg}") - - return {'FINISHED'} - - -class SaveLayersCache(BatchLayerOperator): - fail_msg = "fail to save." - - cache_dir: bpy.props.StringProperty( - name="Cache Directory", - subtype='DIR_PATH', - default="//" - ) # type: ignore - - def operate(self, layer_obj, context): - output_dir = bpy.path.abspath(self.cache_dir) - source_dir = bpy.path.abspath(layer_obj.data.filepath) - - source_path: Path = Path(source_dir).resolve() - is_sequence = layer_obj.data.is_sequence - - name = layer_obj.name if is_sequence else f"{layer_obj.name}.vdb" - output_path: Path = Path(output_dir, name, source_path.name).resolve() \ - if is_sequence else Path(output_dir, name).resolve() - - if output_path != source_path: - copy_to_dir(source_path.parent if is_sequence else source_path, - output_path.parent.parent if is_sequence else output_path.parent, - new_name=name) - - blend_path = Path(bpy.path.abspath("//")).resolve() - - layer_obj.data.filepath = bpy.path.relpath(str(output_path), - start=str(blend_path)) - - return {'FINISHED'} - def invoke(self, context, event): - context.window_manager.invoke_props_dialog(self) - return {'RUNNING_MODAL'} + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + def execute(self, context): + if not self.cache_id: + self.report({"ERROR"}, "Missing cache id") + return {"CANCELLED"} + if not self.directory: + self.report({"ERROR"}, "Please select a folder") + return {"CANCELLED"} -class RemoveLayers(BatchLayerOperator): - fail_msg = "fail to remove." + caches = get_layer_caches() + entry = next((c for c in caches if str( + c.get("id", "")) == self.cache_id), None) + if not entry: + self.report({"ERROR"}, "Layer not found") + return {"CANCELLED"} - def operate(self, layer_obj, context): + old_path = entry["path"] + new_path = ( + bpy.path.relpath(self.directory) + if self.use_relative and bpy.data.filepath + else bpy.path.abspath(self.directory) + ) + entry["path"] = new_path + + set_layer_caches(caches) + refresh_bioxel_panels(context) + + # 更新所有 node group 中 O Layer 节点的 Path for node_group in bpy.data.node_groups: - for node in node_group.nodes: - if get_node_type(node) == "BioxelNodes_FetchLayer": - if node.inputs[0].default_value == layer_obj: - node_group.nodes.remove(node) + for node in get_layer_nodes(node_group): + node_path = getattr(node.inputs.get("Path"), "default_value") + if node_path and node_path == old_path: + node.inputs["Path"].default_value = new_path - cache_filepath = Path(bpy.path.abspath( - layer_obj.data.filepath)).resolve() + self.report({"INFO"}, f"Cache path updated: {new_path}") + return {"FINISHED"} - if cache_filepath.is_file(): - if layer_obj.data.is_sequence: - for f in cache_filepath.parent.iterdir(): - f.unlink(missing_ok=True) - else: - cache_filepath.unlink(missing_ok=True) - # also remove layer object - bpy.data.volumes.remove(layer_obj.data) +class SaveLayer(bpy.types.Operator): + """Copy the entire layer folder (named by id) to a target folder and update its path in the cache""" - return {'FINISHED'} + bl_idname = "bioxel.cache_layer_to_folder" + bl_label = "Save Layer" + bl_options = {"REGISTER", "UNDO"} + + cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore + + use_relative: bpy.props.BoolProperty( + name="Relative Path", + description="Store the path relative to the current Blender file", + default=True, + ) # type: ignore + + directory: bpy.props.StringProperty( + name="Target Folder", + description="Select a folder to cache the layer files", + subtype="DIR_PATH", + ) # type: ignore def invoke(self, context, event): - context.window_manager.invoke_confirm(self, - event, - message=f"Are you sure to remove them?") - return {'RUNNING_MODAL'} + # Default to Blender file directory if possible + blend_dir = Path( + bpy.data.filepath).parent if bpy.data.filepath else Path.cwd() + context.window_manager.fileselect_add(self) + if not self.directory: + self.directory = str(blend_dir) + return {"RUNNING_MODAL"} + + def execute(self, context): + if not self.cache_id: + self.report({"ERROR"}, "Missing cache id") + return {"CANCELLED"} + if not self.directory: + self.report({"ERROR"}, "Please select a target folder") + return {"CANCELLED"} + + caches = get_layer_caches() + entry = next((c for c in caches if str( + c.get("id")) == self.cache_id), None) + cache_id = entry.get("id") + + if not entry: + self.report({"ERROR"}, "Layer not found") + return {"CANCELLED"} + + old_path = entry["path"] + src_path = bpy.path.abspath(old_path) + if not src_path or not Path(src_path).is_dir(): + self.report({"ERROR"}, "Source folder not found") + return {"CANCELLED"} + + src_dir = Path(src_path).resolve() + dst_root = Path(self.directory) + # copy as a subfolder named by id + dst_dir = (dst_root / src_dir.name).resolve() + + try: + if src_dir != dst_dir: + if dst_dir.exists(): + shutil.rmtree(dst_dir) + shutil.copytree(src_dir, dst_dir) + + # Save as relative path if requested and possible + + new_path = ( + bpy.path.relpath(str(dst_dir)) + if self.use_relative and bpy.data.filepath + else bpy.path.abspath(str(dst_dir)) + ) + + entry["path"] = new_path + + set_layer_caches(caches) + refresh_bioxel_panels(context) + + # 遍历所有 node group + for node_group in bpy.data.node_groups: + for node in get_layer_nodes(node_group): + if cache_id == getattr(node.inputs.get("ID"), "default_value"): + node.inputs["Path"].default_value = new_path + + self.report({"INFO"}, f"Layer cached to {dst_dir}") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, f"Failed to cache layer: {e}") + return {"CANCELLED"} -class SaveSelectedLayersCache(bpy.types.Operator, SaveLayersCache): - bl_idname = "bioxelnodes.save_selected_layers_cache" - bl_label = "Save Selected Layers Cache" - bl_description = "Save selected layers' Cache" - bl_icon = "FILE_TICK" +class SelectAndFocusNode(bpy.types.Operator): + """Select and focus the specified node in the current node tree""" - success_msg = "Successfully saved all selected layers." + bl_idname = "bioxel.select_and_focus_node" + bl_label = "Select and Focus Node" + node_name: bpy.props.StringProperty() # type: ignore - def get_layers(self, context): - def is_not_missing(layer_obj, context): - return not is_missing_layer(layer_obj) - return get_selected_layers(context, is_not_missing) - - -class RemoveSelectedLayers(bpy.types.Operator, RemoveLayers): - bl_idname = "bioxelnodes.remove_selected_layers" - bl_label = "Remove Selected Layers" - bl_description = "Remove selected layers" - bl_icon = "TRASH" - - success_msg = "Successfully removed all selected layers." - - def get_layers(self, context): - return get_selected_layers(context) + def execute(self, context): + node_group = get_main_node_group(context) + if not node_group: + self.report({"ERROR"}, "No node group found.") + return {"CANCELLED"} + node = node_group.nodes.get(self.node_name) + if not node: + self.report({"ERROR"}, f"Node '{self.node_name}' not found.") + return {"CANCELLED"} + # Deselect all, select and focus this node + for n in node_group.nodes: + n.select = False + node.select = True + node_group.nodes.active = node + bpy.ops.node.view_selected() + return {"FINISHED"} diff --git a/src/bioxelnodes/operators/misc.py b/src/bioxelnodes/operators/misc.py index c0275e8..7693ece 100644 --- a/src/bioxelnodes/operators/misc.py +++ b/src/bioxelnodes/operators/misc.py @@ -1,110 +1,21 @@ +import json +import uuid import webbrowser import bpy from pathlib import Path import shutil -from ..bioxelutils.common import get_all_layer_objs, get_node_lib_path, get_node_version, is_missing_layer, set_file_prop -from .layer import RemoveLayers, SaveLayersCache - -from ..constants import LATEST_NODE_LIB_PATH, VERSIONS -from ..utils import get_cache_dir - - -class ReLinkNodeLib(bpy.types.Operator): - bl_idname = "bioxelnodes.relink_node_lib" - bl_label = "Relink Node Library" - bl_description = "Relink all nodes to addon library source" - bl_options = {'UNDO'} - - index: bpy.props.IntProperty() # type: ignore - - def execute(self, context): - node_version = VERSIONS[self.index]['node_version'] - lib_path = get_node_lib_path(node_version) \ - if self.index != 0 else LATEST_NODE_LIB_PATH - - node_libs = [] - for node_group in bpy.data.node_groups: - if node_group.name.startswith("BioxelNodes"): - node_lib = node_group.library - if node_lib: - node_libs.append(node_lib) - - node_libs = list(set(node_libs)) - - for node_lib in node_libs: - node_lib.filepath = str(lib_path) - # FIXME: may cause crash - node_lib.reload() - - set_file_prop("node_version", node_version) - - self.report({"INFO"}, f"Successfully relinked.") - - return {'FINISHED'} - - -class SaveNodeLib(bpy.types.Operator): - bl_idname = "bioxelnodes.save_node_lib" - bl_label = "Save Node Library" - bl_description = "Save node library file to local" - bl_options = {'UNDO'} - - lib_dir: bpy.props.StringProperty( - name="Library Directory", - subtype='DIR_PATH', - default="//" - ) # type: ignore - - def execute(self, context): - node_version = get_node_version() - - if node_version is None: - node_version = VERSIONS[0]["node_version"] - else: - if node_version not in [v["node_version"] for v in VERSIONS]: - node_version = VERSIONS[0]["node_version"] - - if node_version == VERSIONS[0]["node_version"]: - lib_path = LATEST_NODE_LIB_PATH - else: - lib_path = get_node_lib_path(node_version) - - version_str = "v"+".".join([str(i) for i in list(node_version)]) - - lib_dir = bpy.path.abspath(self.lib_dir) - local_lib_path: Path = Path(lib_dir, - f"BioxelNodes_{version_str}.blend").resolve() - node_lib_path: Path = lib_path - blend_path = Path(bpy.path.abspath("//")).resolve() - - if local_lib_path != node_lib_path: - shutil.copy(node_lib_path, local_lib_path) - - libs = [] - for node_group in bpy.data.node_groups: - if node_group.name.startswith("BioxelNodes"): - if node_group.library: - libs.append(node_group.library) - - libs = list(set(libs)) - for lib in libs: - lib.filepath = bpy.path.relpath(str(local_lib_path), - start=str(blend_path)) - - return {'FINISHED'} - - def invoke(self, context, event): - context.window_manager.invoke_props_dialog(self) - return {'RUNNING_MODAL'} - - @classmethod - def poll(cls, context): - return bpy.data.is_saved +from ..constants import LATEST_NODE_LIB_PATH +from ..utils import ( + get_all_layer_objs, + get_cache_dir, + get_node_version, + is_missing_layer, +) class CleanTemp(bpy.types.Operator): - bl_idname = "bioxelnodes.clear_temp" + bl_idname = "bioxel.clear_temp" bl_label = "Clean Temp" bl_description = "Clean all cache in temp (include other project cache)" @@ -113,24 +24,26 @@ class CleanTemp(bpy.types.Operator): try: shutil.rmtree(cache_dir) self.report({"INFO"}, f"Successfully cleaned temp.") - return {'FINISHED'} + return {"FINISHED"} except: - self.report({"WARNING"}, - "Fail to clean temp, you may do it manually.") - return {'CANCELLED'} + self.report( + {"WARNING"}, "Fail to clean temp, you may do it manually.") + return {"CANCELLED"} def invoke(self, context, event): - context.window_manager.invoke_confirm(self, - event, - message="All temp files will be removed, include other project cache, do you still want to clean?") - return {'RUNNING_MODAL'} + context.window_manager.invoke_confirm( + self, + event, + message="All temp files will be removed, include other project cache, do you still want to clean?", + ) + return {"RUNNING_MODAL"} class RenderSettingPreset(bpy.types.Operator): - bl_idname = "bioxelnodes.render_setting_preset" + bl_idname = "bioxel.render_setting_preset" bl_label = "Render Setting Presets" bl_description = "Render setting presets for bioxel" - bl_options = {'UNDO'} + bl_options = {"UNDO"} PRESETS = { "performance": "Performance", @@ -138,166 +51,201 @@ class RenderSettingPreset(bpy.types.Operator): "quality": "Quality", } - preset: bpy.props.EnumProperty(name="Preset", - default="balance", - items=[(k, v, "") - for k, v in PRESETS.items()]) # type: ignore + preset: bpy.props.EnumProperty( + name="Preset", default="balance", items=[(k, v, "") for k, v in PRESETS.items()] + ) # type: ignore def execute(self, context): if self.preset == "performance": # EEVEE - # bpy.context.scene.eevee.use_taa_reprojection = False - bpy.context.scene.eevee.use_shadow_jitter_viewport = True - bpy.context.scene.eevee.volumetric_tile_size = '2' + bpy.context.scene.eevee.use_taa_reprojection = False + # bpy.context.scene.eevee.use_shadow_jitter_viewport = True + bpy.context.scene.eevee.volumetric_tile_size = "4" bpy.context.scene.eevee.volumetric_shadow_samples = 32 bpy.context.scene.eevee.volumetric_samples = 64 bpy.context.scene.eevee.volumetric_ray_depth = 1 bpy.context.scene.eevee.use_volumetric_shadows = True # Cycles - bpy.context.scene.cycles.volume_bounces = 0 + bpy.context.scene.cycles.volume_bounces = 4 bpy.context.scene.cycles.transparent_max_bounces = 4 - bpy.context.scene.cycles.volume_preview_step_rate = 4 - bpy.context.scene.cycles.volume_step_rate = 4 + bpy.context.scene.cycles.volume_biased = True + bpy.context.scene.cycles.volume_preview_step_rate = 100 + bpy.context.scene.cycles.volume_step_rate = 100 elif self.preset == "balance": # EEVEE - # bpy.context.scene.eevee.use_taa_reprojection = False - bpy.context.scene.eevee.use_shadow_jitter_viewport = True - bpy.context.scene.eevee.volumetric_tile_size = '2' + bpy.context.scene.eevee.use_taa_reprojection = False + # bpy.context.scene.eevee.use_shadow_jitter_viewport = True + bpy.context.scene.eevee.volumetric_tile_size = "2" bpy.context.scene.eevee.volumetric_shadow_samples = 64 bpy.context.scene.eevee.volumetric_samples = 128 bpy.context.scene.eevee.volumetric_ray_depth = 8 bpy.context.scene.eevee.use_volumetric_shadows = True # Cycles - bpy.context.scene.cycles.volume_bounces = 4 - bpy.context.scene.cycles.transparent_max_bounces = 8 - bpy.context.scene.cycles.volume_preview_step_rate = 1 - bpy.context.scene.cycles.volume_step_rate = 1 + bpy.context.scene.cycles.volume_bounces = 8 + bpy.context.scene.cycles.transparent_max_bounces = 16 + bpy.context.scene.cycles.volume_biased = True + bpy.context.scene.cycles.volume_preview_step_rate = 10 + bpy.context.scene.cycles.volume_step_rate = 10 elif self.preset == "quality": # EEVEE - # bpy.context.scene.eevee.use_taa_reprojection = False - bpy.context.scene.eevee.use_shadow_jitter_viewport = True - bpy.context.scene.eevee.volumetric_tile_size = '2' + bpy.context.scene.eevee.use_taa_reprojection = False + # bpy.context.scene.eevee.use_shadow_jitter_viewport = True + bpy.context.scene.eevee.volumetric_tile_size = "1" bpy.context.scene.eevee.volumetric_shadow_samples = 128 bpy.context.scene.eevee.volumetric_samples = 256 bpy.context.scene.eevee.volumetric_ray_depth = 16 bpy.context.scene.eevee.use_volumetric_shadows = True # Cycles - bpy.context.scene.cycles.volume_bounces = 8 - bpy.context.scene.cycles.transparent_max_bounces = 16 - bpy.context.scene.cycles.volume_preview_step_rate = 0.5 - bpy.context.scene.cycles.volume_step_rate = 0.5 + bpy.context.scene.cycles.volume_bounces = 16 + bpy.context.scene.cycles.transparent_max_bounces = 32 + bpy.context.scene.cycles.volume_biased = False - return {'FINISHED'} - - -class SliceViewer(bpy.types.Operator): - bl_idname = "bioxelnodes.slice_viewer" - bl_label = "Slice Viewer" - bl_description = "A preview scene setting for viewing slicers" - bl_icon = "FILE_VOLUME" - - def execute(self, context): - # bpy.context.scene.eevee.use_taa_reprojection = False - bpy.context.scene.eevee.volumetric_tile_size = '2' - bpy.context.scene.eevee.volumetric_shadow_samples = 128 - bpy.context.scene.eevee.volumetric_samples = 128 - bpy.context.scene.eevee.volumetric_ray_depth = 1 - - bpy.context.scene.eevee.use_shadows = False - bpy.context.scene.eevee.use_volumetric_shadows = False - - view_3d = None - if context.area.type == 'VIEW_3D': - view_3d = context.area - else: - for area in context.screen.areas: - if area.type == 'VIEW_3D': - view_3d = area - break - if view_3d: - view_3d.spaces[0].shading.type = 'MATERIAL' - view_3d.spaces[0].shading.studio_light = 'studio.exr' - view_3d.spaces[0].shading.studiolight_intensity = 1.5 - view_3d.spaces[0].shading.use_scene_lights = False - view_3d.spaces[0].shading.use_scene_world = False - - return {'FINISHED'} - - -def get_all_layers(layer_filter=None): - def _layer_filter(layer_obj): - return True - - layer_filter = layer_filter or _layer_filter - layer_objs = get_all_layer_objs() - return [obj for obj in layer_objs if layer_filter(obj)] - - -class SaveAllLayersCache(bpy.types.Operator, SaveLayersCache): - bl_idname = "bioxelnodes.save_all_layers_cache" - bl_label = "Save All Layers Cache" - bl_description = "Save all cache of this file" - bl_icon = "FILE_TICK" - - success_msg = "Successfully saved all layers." - - def get_layers(self, context): - def is_not_missing(layer_obj): - return not is_missing_layer(layer_obj) - return get_all_layers(is_not_missing) - - -class RemoveAllMissingLayers(bpy.types.Operator, RemoveLayers): - bl_idname = "bioxelnodes.remove_all_missing_layers" - bl_label = "Remove All Missing Layers" - bl_description = "Remove all current container missing layers" - bl_icon = "BRUSH_DATA" - - success_msg = "Successfully removed all missing layers." - - def get_layers(self, context): - def is_missing(layer_obj): - return is_missing_layer(layer_obj) - return get_all_layers(is_missing) - - def invoke(self, context, event): - context.window_manager.invoke_confirm(self, - event, - message=f"Are you sure to remove all **Missing** layers?") - return {'RUNNING_MODAL'} + return {"FINISHED"} class Help(bpy.types.Operator): - bl_idname = "bioxelnodes.help" + bl_idname = "bioxel.help" bl_label = "Need Help?" bl_description = "Online Manual for Beginner" bl_icon = "HELP" def execute(self, context): - webbrowser.open( - 'https://omoolab.github.io/BioxelNodes/latest/', new=2) + webbrowser.open("https://omoolab.github.io/BioxelNodes/latest/", new=2) - return {'FINISHED'} + return {"FINISHED"} -class AddEeveeEnv(bpy.types.Operator): - bl_idname = "bioxelnodes.add_eevee_env" - bl_label = "Add EEVEE Env Light" - bl_description = "To make volume shadow less dark" +# 定义虚无化切换操作 +class TogglePhantom(bpy.types.Operator): + """Toggle object phantom state (ray invisible, no shadows, semi-transparent wireframe)""" + + bl_idname = "bioxel.toggle_phantom" + bl_label = "Toggle Phantom" + bl_options = {"REGISTER", "UNDO"} def execute(self, context): - bpy.ops.wm.append( - directory=f"{str(LATEST_NODE_LIB_PATH)}/Object", - filename="EeveeEnv" + obj = context.active_object + if not obj: + self.report({"WARNING"}, "No object selected") + return {"CANCELLED"} + + # 获取3D视图空间 + view_space = None + for area in bpy.context.screen.areas: + if area.type == "VIEW_3D": + for space in area.spaces: + if space.type == "VIEW_3D": + view_space = space + break + if view_space: + break + + # 确保存储状态的文本数据块存在 + text_name = "phantom_orig_states" + if text_name not in bpy.data.texts: + bpy.data.texts.new(text_name) + + # 从文本数据块加载状态 + states_text = bpy.data.texts[text_name] + try: + states = json.loads(states_text.as_string()) + except: + states = {} + + def get_obj_uuid(obj): + if "bioxel_uuid" not in obj: + obj["bioxel_uuid"] = uuid.uuid4().hex + return obj["bioxel_uuid"] + + # 使用物体的唯一标识符来追踪 + obj_uuid = get_obj_uuid(obj) + + # 检查物体是否已经处于虚无化状态 + is_phantom = ( + obj.display_type == "SOLID" + and obj.show_wire + and obj.show_in_front + and round(obj.color[3], 2) == 0.10 + and not obj.visible_camera + and not obj.visible_diffuse + and not obj.visible_glossy + and not obj.visible_transmission + and not obj.visible_volume_scatter + and not obj.visible_shadow ) - bpy.context.scene.eevee.use_shadows = True - bpy.context.scene.eevee.use_volumetric_shadows = True + if is_phantom: + # 恢复原始状态 + if obj_uuid in states: + state = states[obj_uuid] + obj.display_type = state["display_type"] + obj.show_wire = state["show_wire"] + obj.show_in_front = state["show_in_front"] + obj.color = state["color"] + obj.visible_camera = state["visible_camera"] + obj.visible_diffuse = state["visible_diffuse"] + obj.visible_glossy = state["visible_glossy"] + obj.visible_transmission = state["visible_transmission"] + obj.visible_volume_scatter = state["visible_volume_scatter"] + obj.visible_shadow = state["visible_shadow"] - return {'FINISHED'} + # 恢复视图着色设置(如果之前有保存) + if view_space and "viewport_color_type" in state: + view_space.shading.color_type = state["viewport_color_type"] + + del states[obj_uuid] + self.report({"INFO"}, f"Object restored") + else: + # 保存原始状态 + state_data = { + "display_type": obj.display_type, + "show_wire": obj.show_wire, + "show_in_front": obj.show_in_front, + "color": list(obj.color), # 保存RGBA颜色值 + "visible_camera": obj.visible_camera, + "visible_diffuse": obj.visible_diffuse, + "visible_glossy": obj.visible_glossy, + "visible_transmission": obj.visible_transmission, + "visible_volume_scatter": obj.visible_volume_scatter, + "visible_shadow": obj.visible_shadow, + } + + # 检查并处理视图着色设置 + if view_space: + # 保存当前视图着色设置 + state_data["viewport_color_type"] = view_space.shading.color_type + + # 检查当前是否为object, random或attribute + valid_types = {"OBJECT", "RANDOM", "ATTRIBUTE"} + if view_space.shading.color_type not in valid_types: + # 改为object模式 + view_space.shading.color_type = "OBJECT" + + states[obj_uuid] = state_data + + # 设置虚无化状态 + obj.display_type = "SOLID" # 以实体方式显示 + obj.show_wire = True # 显示线框 + obj.show_in_front = True # 显示在其他物体前面 + # 保持RGB不变,仅修改alpha透明度为0.1 + obj.color[3] = 0.1 + # 关闭所有射线可见性和投影 + obj.visible_camera = False + obj.visible_diffuse = False + obj.visible_glossy = False + obj.visible_transmission = False + obj.visible_volume_scatter = False + obj.visible_shadow = False + self.report({"INFO"}, f"Object phantomed") + + # 保存状态到文本数据块 + states_text.clear() + states_text.write(json.dumps(states, indent=2)) + + return {"FINISHED"} diff --git a/src/bioxelnodes/operators/node.py b/src/bioxelnodes/operators/node.py index e0d1229..09ba8ef 100644 --- a/src/bioxelnodes/operators/node.py +++ b/src/bioxelnodes/operators/node.py @@ -1,12 +1,12 @@ import bpy -from ..bioxelutils.common import is_incompatible, local_lib_not_updated -from ..bioxelutils.node import assign_node_tree, get_node_tree +from ..utils import is_incompatible, local_lib_not_updated +from ..node import assign_node_tree, get_node_tree from ..utils import get_use_link class AddNode(bpy.types.Operator): - bl_idname = "bioxelnodes.add_node" + bl_idname = "bioxel.add_node" bl_label = "Add Node" bl_options = {"REGISTER", "UNDO"} @@ -24,7 +24,7 @@ class AddNode(bpy.types.Operator): @property def node_type(self): - return f"BioxelNodes_{self.node_name}" + return f"O {self.node_name}" def execute(self, context): space = context.space_data diff --git a/src/bioxelnodes/operators/structure.py b/src/bioxelnodes/operators/structure.py new file mode 100644 index 0000000..c4acf87 --- /dev/null +++ b/src/bioxelnodes/operators/structure.py @@ -0,0 +1,107 @@ +import bpy +from ..utils import get_use_link +from ..node import add_node_to_graph, get_output_node, get_main_node_group + + +class ExtractMesh(bpy.types.Operator): + bl_idname = "bioxel.extract_mesh" + bl_label = "Extract Mesh" + bl_description = "Extract Mesh" + bl_icon = "OUTLINER_OB_MESH" + bl_options = {"UNDO"} + + @classmethod + def poll(cls, context): + # Check if there is a selected node in the node group + node_group = get_main_node_group(context) + selected_nodes = [n for n in node_group.nodes if n.select] + + if len(selected_nodes) == 0: + return False + else: + source_node = selected_nodes[0] + return "Structure" in source_node.outputs + + def execute(self, context): + container_obj = context.object + + if container_obj is None: + self.report({"WARNING"}, "Cannot find any bioxel container.") + return {"FINISHED"} + + node_group = container_obj.modifiers[0].node_group + selected_nodes = [n for n in node_group.nodes if n.select] + + source_node = selected_nodes[0] + + # Check if the selected node has a "Structure" output + if "Structure" not in source_node.outputs: + self.report({"ERROR"}, "Selected node has no 'Structure' output.") + return {"CANCELLED"} + + # 记录原始输出连接(用于后续复原) + output_node = get_output_node(node_group) + original_links = list(output_node.inputs[0].links) + + # 添加Component to Mesh节点 + to_mesh_node = add_node_to_graph( + "Structure to Mesh", node_group, use_link=get_use_link() + ) + + # 连接节点:源节点 -> Component to Mesh -> 输出节点 + node_group.links.new( + source_node.outputs["Structure"], to_mesh_node.inputs[0]) + node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0]) + + new_obj = None + + try: + deps = context.evaluated_depsgraph_get() + eval_obj = container_obj.evaluated_get(deps) + # obtain evaluated mesh (from modifiers including Geometry Nodes) + mesh_eval = eval_obj.to_mesh() + if mesh_eval is None: + raise RuntimeError("Failed to produce evaluated mesh") + + mesh_name = f"{container_obj.name}_Mesh" + mesh_copy = mesh_eval.copy() + mesh_copy.name = mesh_name + # clear the temporary evaluated mesh + try: + eval_obj.to_mesh_clear() + except Exception: + pass + + # create the new object with same transform as source + new_obj = bpy.data.objects.new(mesh_name, mesh_copy) + # link to same collection as the container (fallback to current collection) + col = ( + container_obj.users_collection[0] + if getattr(container_obj, "users_collection", None) + else context.collection + ) + if col is None: + col = context.collection + col.objects.link(new_obj) + + # copy world transform exactly + new_obj.matrix_world = container_obj.matrix_world.copy() + except Exception as e: + self.report({"ERROR"}, f"Failed to extract mesh: {e}") + + # 恢复原始连接 + for link in original_links: + node_group.links.new(link.from_socket, output_node.inputs[0]) + + # 删除临时添加的Component to Mesh节点 + node_group.nodes.remove(to_mesh_node) + if new_obj: + # deselect everything, select and activate new_obj + for o in list(context.selected_objects): + o.select_set(False) + new_obj.select_set(True) + context.view_layer.objects.active = new_obj + self.report({"INFO"}, f"Mesh extracted: {new_obj.name}") + return {"FINISHED"} + else: + return {"CANCELLED"} diff --git a/src/bioxelnodes/panels.py b/src/bioxelnodes/panels.py new file mode 100644 index 0000000..5c87684 --- /dev/null +++ b/src/bioxelnodes/panels.py @@ -0,0 +1,180 @@ +from pathlib import Path +import bpy + +from .node import get_layer_nodes, get_main_node_group +from .utils import load_icon +from .layer import get_layer_caches +from .operators.io import ImportAsColor, ImportAsLabel, ImportAsScalar, ImportData +from .operators.misc import Help, RenderSettingPreset +from .operators.layer import ( + AddLayerNode, + DeleteLayer, + RelocatePath, + RenameLayer, + SaveLayer, +) + + +class ImportMenu(bpy.types.Menu): + bl_label = "Bioxel Import" + bl_idname = "BIOXEL_MT_import" + + def draw(self, context): + layout = self.layout + layout.operator(ImportAsScalar.bl_idname, + text="Import As Scalar").filepath = "" + layout.operator(ImportAsLabel.bl_idname, + text="Import As Label").filepath = "" + layout.operator(ImportAsColor.bl_idname, + text="Import As Color").filepath = "" + + +class RenderSettingMenu(bpy.types.Menu): + bl_label = "Render Setting Presets" + bl_idname = "BIOXEL_MT_render_setting" + + def draw(self, context): + layout = self.layout + for k, v in RenderSettingPreset.PRESETS.items(): + op = layout.operator(RenderSettingPreset.bl_idname, text=v) + op.preset = k + + +class BioxelPanelBase: + """ + Abstract base panel for all Bioxel UI panels. + + - Ensures the panel appears only when the active area is the Node Editor + and the Node Editor is editing a Geometry Nodes tree. + - Places the panel under the "Bioxel" sidebar tab. + - Provides an optional bl_order for panel ordering inside the category. + """ + + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_context = "geometry_node" + bl_category = "胞素(Bioxel)" + # try to make Bioxel panels order early (may be respected by Blender) + + @classmethod + def poll(cls, context): + node_group = get_main_node_group(context) + return node_group is not None + + +class HeaderPanel(bpy.types.Panel, BioxelPanelBase): + bl_label = "胞素(Bioxel)" + bl_idname = "BIOXEL_PT_header_panel" + bl_order = 0 + + def draw(self, context): + layout = self.layout + + layout.operator( + Help.bl_idname, text="Help", icon=Help.bl_icon # 复用原操作的图标定义 + ) + + layout.menu( + RenderSettingMenu.bl_idname, text="Render Preset", icon="RENDER_STILL" + ) + + +class LayerNodePanel(BioxelPanelBase, bpy.types.Panel): + bl_label = "Layer Nodes" + bl_idname = "BIOXEL_PT_o_layer_nodes" + bl_order = 1 + + def draw(self, context): + layout = self.layout + node_group = get_main_node_group(context) + + layout.template_list( + "BIOXEL_UL_layer_nodes", + "", + node_group, + "nodes", + context.window_manager, + "bioxel_layer_list_index", + rows=3, + ) + + +class LibraryPanel(BioxelPanelBase, bpy.types.Panel): + bl_label = "Layer Library" + bl_idname = "BIOXEL_PT_library_panel" + bl_order = 2 + + def draw(self, context): + layout = self.layout + wm = context.window_manager + caches = get_layer_caches() + + row = layout.row() + row.scale_y = 2.0 + row.operator(ImportData.bl_idname, icon="IMPORT") + # row.menu(ImportMenu.bl_idname, icon="IMPORT", text="Import") + layout.separator() + layout.template_icon_view( + bpy.context.window_manager, + "bioxel_layer_library", + show_labels=False, + scale=10.0, + scale_popup=6.0, + ) + + selected_id = getattr(wm, "bioxel_layer_library", None) + selected_id = None if selected_id == "nothing_found" else selected_id + entry = next((c for c in caches if c.get("id") == selected_id), None) + if entry: + cache_id = entry["id"] + path = entry["path"] + name = entry.get("name", "?") + kind = entry.get("kind", "?") + path_exists = Path(bpy.path.abspath(path)).exists() + + layout.prop(wm, "bioxel_snapshot_z", text="Z Slice", slider=True) + layout.separator() + # operations and meta_box as before + ops_row = layout.row(align=True) + if path_exists: + ops_row.operator( + AddLayerNode.bl_idname, text="Add", icon="PLUS" + ).cache_id = selected_id + ops_row.operator( + SaveLayer.bl_idname, text="Save", icon="FILE_TICK" + ).cache_id = selected_id + ops_row.operator( + RenameLayer.bl_idname, text="Name", icon="FONT_DATA" + ).cache_id = selected_id + ops_row.operator( + DeleteLayer.bl_idname, text="", icon="TRASH" + ).cache_id = selected_id + else: + ops_row.operator( + RelocatePath.bl_idname, text="Relocate", icon="FILE_FOLDER" + ).cache_id = selected_id + ops_row.operator( + DeleteLayer.bl_idname, text="", icon="TRASH" + ).cache_id = selected_id + + meta_box = layout.box() + if hasattr(meta_box, "alert"): + meta_box.alert = True + meta_box.label(text=f"Cache is missing...") + meta_box.label(text=f"Please relocate the cache path!") + return + + meta_box = layout.box() + meta_box.label(text=f"Name: {name}") + meta_box.label(text=f"Path: {path}") + meta_box.label(text=f"Kind: {kind}") + meta_box.label( + text=f"Dims: [{entry.get('frame_count',1)},{tuple(entry.get('shape',''))},{entry.get('channel_count',1)}]" + ) + if kind in ["scalar", "color", "vector"]: + path = Path(bpy.path.abspath(entry["path"])) + histogram = load_icon( + path / "histogram.png", f"{cache_id}_histogram") + meta_box.template_icon(histogram, scale=10.0) + else: + layout.label(text="No layer selected", icon="QUESTION") diff --git a/src/bioxelnodes/preferences.py b/src/bioxelnodes/preferences.py index ff9f9ea..650e990 100644 --- a/src/bioxelnodes/preferences.py +++ b/src/bioxelnodes/preferences.py @@ -8,22 +8,10 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences): cache_dir: bpy.props.StringProperty( name="Set Cache Directory", subtype='DIR_PATH', - default=str(Path(Path.home(), '.bioxelnodes')) + default=str(Path(Path.home(), '.bioxel')) ) # type: ignore - change_render_setting: bpy.props.BoolProperty( - name="Change Render Setting on First Import", - default=True, - ) # type: ignore - - node_import_method: bpy.props.EnumProperty(name="Node Import Method", - default="LINK", - items=[("LINK", "Link", ""), - ("APPEND", "Append (Reuse Data)", "")]) # type: ignore - def draw(self, context): layout = self.layout layout.label(text="Configuration") layout.prop(self, 'cache_dir') - layout.prop(self, "change_render_setting") - layout.prop(self, "node_import_method") diff --git a/src/bioxelnodes/props.py b/src/bioxelnodes/props.py index e931831..e79b38b 100644 --- a/src/bioxelnodes/props.py +++ b/src/bioxelnodes/props.py @@ -1,44 +1,134 @@ +from pathlib import Path import bpy -from .bioxelutils.common import get_node_type +from .operators.layer import SelectAndFocusNode +from .utils import load_icon +from .layer import get_layer_caches, set_layer_caches -class BIOXELNODES_UL_layer_list(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - layout.label(text=item.label, translate=False, icon_value=icon) +def get_snapshot_icon(cache, z: float): + cache_id = str(cache["id"]) + shape = (64, 64, 32) + zidx = int(z * (shape[2] - 1)) + + path = Path(bpy.path.abspath(cache["path"])) + return load_icon(path / f"snapshot_{zidx}.png", f"{cache_id}_{zidx}") -class BIOXELNODES_Series(bpy.types.PropertyGroup): - id: bpy.props.StringProperty() # type: ignore - label: bpy.props.StringProperty() # type: ignore +def _bioxel_layer_items(self, context): + """ + EnumProperty items callback: 从 get_layer_caches 构造枚举项。 + 使用 PREVIEWS 加载每个 layer 的 thumbnail_png(如果存在)并返回 icon_id。 + """ + items = [] + caches = get_layer_caches() + + for idx, cache in enumerate(caches): + cache_id = str(cache["id"]) + name = cache.get("name", f"Layer ?") + description = cache.get("kind", "?") + z = cache.get("snapshot_z", 0.5) + icon = get_snapshot_icon(cache, z) + # 在这个context下,只能加载无法生成。 + items.append((cache_id, name, description, icon, idx)) + + if not items: + items = [("nothing_found", "No Layers", + "No layers found", "QUESTION", 0)] + + return items -def select_layer(self, context): - layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL - layer_list = layer_list_UL.layer_list - layer_list_active = layer_list_UL.layer_list_active - - if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list): - layer_obj = bpy.data.objects[layer_list[layer_list_active].obj_name] - node_group = context.space_data.edit_tree - for node in node_group.nodes: - node.select = False - if get_node_type(node) == "BioxelNodes_FetchLayer": - if node.inputs[0].default_value == layer_obj: - node.select = True - - layer_list_UL.layer_list_active = -1 +def _update_layer_gallery(self, context): + wm = context.window_manager + selected_id = getattr(wm, "bioxel_layer_library", None) + cache = next( + (c for c in get_layer_caches() if str(c.get("id", "")) == str(selected_id)), + None, + ) + z = cache.get("snapshot_z", 0.5) if cache else 0.5 + setattr(wm, "bioxel_snapshot_z", z) -class BIOXELNODES_Layer(bpy.types.PropertyGroup): - obj_name: bpy.props.StringProperty() # type: ignore - label: bpy.props.StringProperty() # type: ignore - info_text: bpy.props.StringProperty() # type: ignore +def _update_snapshot_z(self, context): + """ + update callback for WindowManager.bioxel_thumb_z + - write a slice PNG from the saved .npy + - update PREVIEWS entry for the selected layer + - force template_icon_view to refresh by briefly cycling the EnumProperty value + - force UI redraw + """ + wm = context.window_manager + selected_id = getattr(wm, "bioxel_layer_library", None) + + if not selected_id or selected_id == "nothing_found": + return + + try: + selected_id = int(selected_id) + except Exception: + return + + caches = get_layer_caches() + z = getattr(wm, "bioxel_snapshot_z", 0.5) + + for cache in caches: + if int(cache.get("id", -1)) == selected_id: + cache["snapshot_z"] = z + break + + set_layer_caches(caches) -class BIOXELNODES_LayerListUL(bpy.types.PropertyGroup): - layer_list: bpy.props.CollectionProperty( - type=BIOXELNODES_Layer) # type: ignore - layer_list_active: bpy.props.IntProperty(default=-1, - update=select_layer, - options=set()) # type: ignore +class BIOXEL_Series(bpy.types.PropertyGroup): + id: bpy.props.StringProperty() # type: ignore + label: bpy.props.StringProperty() # type: ignore + + +class BIOXEL_UL_layer_nodes(bpy.types.UIList): + """UIList for O Layer nodes only""" + + bl_idname = "BIOXEL_UL_layer_nodes" + + def filter_items(self, context, data, propname): + nodes = getattr(data, propname) + flt_flags = [] + flt_neworder = [] + + for node in nodes: + # 只保留 O Layer 节点 + if ( + hasattr(node, "bl_idname") + and node.bl_idname.startswith("GeometryNodeGroup") + and getattr(getattr(node, "node_tree", None), "name", "").startswith( + "O Layer" + ) + ): + flt_flags.append(self.bitflag_filter_item) + else: + flt_flags.append(0) + return flt_flags, flt_neworder + + def draw_item( + self, context, layout, data, item, icon, active_data, active_propname, index + ): + node = item + # 这里只画 O Layer 节点,其他已被 filter_items 过滤 + split = layout.split(factor=0.3, align=True) + col1 = split.row(align=True) + col2 = split.row(align=True) + name_socket = node.inputs["Name"] + resample_socket = node.inputs["Resample"] + scale_socket = node.inputs["Bioxel Scale"] + name = str(node.inputs["Name"].default_value) + + # row.prop(name_socket, "default_value", text="") + col1.prop(resample_socket, "default_value", + icon="UV_DATA", text="", toggle=1) + col1.prop(scale_socket, "default_value", text="") + col2.label(text=name) + + op = col2.operator( + SelectAndFocusNode.bl_idname, text="", icon="RESTRICT_SELECT_OFF", emboss=True + ) + op.node_name = node.name diff --git a/src/bioxelnodes/utils.py b/src/bioxelnodes/utils.py index 2bce786..953e792 100644 --- a/src/bioxelnodes/utils.py +++ b/src/bioxelnodes/utils.py @@ -1,6 +1,12 @@ from pathlib import Path +from ast import literal_eval + import shutil import bpy +import numpy as np + +from .node import get_nodes_by_type +from .constants import LATEST_NODE_LIB_PATH, NODE_LIB_DIRPATH, PREVIEW_COLLECTIONS def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True): @@ -44,16 +50,16 @@ def select_object(target_obj): def progress_bar(self, context): row = self.layout.row() row.progress( - factor=context.window_manager.bioxelnodes_progress_factor, + factor=context.window_manager.bioxel_progress_factor, type="BAR", - text=context.window_manager.bioxelnodes_progress_text + text=context.window_manager.bioxel_progress_text ) row.scale_x = 2 def progress_update(context, factor, text=""): - context.window_manager.bioxelnodes_progress_factor = factor - context.window_manager.bioxelnodes_progress_text = text + bpy.context.window_manager.bioxel_progress_factor = factor + bpy.context.window_manager.bioxel_progress_text = text def get_preferences(): @@ -62,11 +68,335 @@ def get_preferences(): def get_cache_dir(): preferences = get_preferences() - cache_path = Path(preferences.cache_dir, 'VDBs') + cache_path = Path(preferences.cache_dir) cache_path.mkdir(parents=True, exist_ok=True) - return str(cache_path) + return cache_path def get_use_link(): preferences = get_preferences() return preferences.node_import_method == "LINK" + + +def get_container_objs_from_selection(): + container_objs = [] + for obj in bpy.context.selected_objects: + if get_container_obj(obj): + container_objs.append(obj) + + return list(set(container_objs)) + + +def get_container_obj(current_obj): + if current_obj: + if current_obj.get('bioxel_container'): + return current_obj + elif current_obj.get('bioxel_layer'): + parent = current_obj.parent + return parent if parent.get('bioxel_container') else None + return None + + +def get_layer_prop_value(layer_obj: bpy.types.Object, prop_name: str): + node_group = layer_obj.modifiers[0].node_group + layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0] + prop = layer_node.inputs.get(prop_name) + if prop is None: + return None + + value = prop.default_value + + if type(value).__name__ == "bpy_prop_array": + value = tuple(value) + return tuple([int(v) for v in value]) \ + if prop in ["shape"] else value + elif type(value).__name__ == "str": + return str(value) + elif type(value).__name__ == "float": + value = float(value) + return round(value, 2) \ + if prop in ["bioxel_size"] else value + elif type(value).__name__ == "int": + value = int(value) + return value + else: + return value + + +def get_layer_name(layer_obj): + return get_layer_prop_value(layer_obj, "name") + + +def get_layer_kind(layer_obj): + return get_layer_prop_value(layer_obj, "kind") + + +def get_layer_label(layer_obj): + name = get_layer_name(layer_obj) + # kind = get_layer_kind(layer_obj) + + label = f"{name}" + + if is_missing_layer(layer_obj): + return "**MISSING**" + label + elif is_temp_layer(layer_obj): + return "* " + label + else: + return label + + +def is_missing_layer(layer_obj): + cache_filepath = Path(bpy.path.abspath( + layer_obj.data.filepath)).resolve() + return not cache_filepath.is_file() + + +def is_temp_layer(layer_obj): + cache_filepath = Path(bpy.path.abspath( + layer_obj.data.filepath)).resolve() + cache_dirpath = Path(get_cache_dir()) + return cache_dirpath in cache_filepath.parents + + +def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value): + node_group = layer_obj.modifiers[0].node_group + layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0] + layer_node.inputs[prop].default_value = value + + +def get_layer_obj(current_obj: bpy.types.Object): + if current_obj: + if current_obj.get('bioxel_layer') and current_obj.parent: + if current_obj.parent.get('bioxel_container'): + return current_obj + return None + + +def get_container_layer_objs(container_obj: bpy.types.Object): + layer_objs = [] + for obj in bpy.data.objects: + if obj.parent == container_obj and get_layer_obj(obj): + layer_objs.append(obj) + + return layer_objs + + +def get_all_layer_objs(): + layer_objs = [] + for obj in bpy.data.objects: + if get_layer_obj(obj): + layer_objs.append(obj) + + return layer_objs + + +def add_driver(target, target_prop, var_sources, expression): + driver = target.driver_add(target_prop) + is_vector = isinstance(driver, list) + drivers = driver if is_vector else [driver] + + for i, driver in enumerate(drivers): + for j, var_source in enumerate(var_sources): + + source = var_source['source'] + prop = var_source['prop'] + + var = driver.driver.variables.new() + var.name = f"var{j}" + + var.targets[0].id_type = source.id_type + var.targets[0].id = source + var.targets[0].data_path = f'["{prop}"][{i}]'\ + if is_vector else f'["{prop}"]' + + driver.driver.expression = expression + + +def add_direct_driver(target, target_prop, source, source_prop): + drivers = [ + { + "source": source, + "prop": source_prop + } + ] + expression = "var0" + add_driver(target, target_prop, drivers, expression) + + +def read_file_prop(content: str): + props = {} + for line in content.split("\n"): + line = line.replace(" ", "") + p = line.split("=")[0] + if p != "": + v = line.split("=")[-1] + props[p] = v + return props + + +def write_file_prop(props: dict): + lines = [] + for p, v in props.items(): + lines.append(f"{p} = {v}") + return "\n".join(lines) + + +def set_file_prop(prop, value): + if bpy.data.texts.get("BioxelNodes") is None: + bpy.data.texts.new("BioxelNodes") + + props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string()) + props[prop] = value + bpy.data.texts["BioxelNodes"].clear() + bpy.data.texts["BioxelNodes"].write(write_file_prop(props)) + + +def get_file_prop(prop): + if bpy.data.texts.get("BioxelNodes") is None: + bpy.data.texts.new("BioxelNodes") + + props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string()) + return props.get(prop) + + +def get_node_version(): + node_version = get_file_prop("node_version") + return literal_eval(node_version) if node_version else None + + +def is_incompatible(): + return False + + +def get_node_lib_path(node_version): + version_str = "v"+".".join([str(i) for i in list(node_version)]) + lib_filename = f"BioxelNodes_{version_str}.blend" + return Path(NODE_LIB_DIRPATH, + lib_filename).resolve() + + +def local_lib_not_updated(): + return False + + +def ndarray_to_png(array: np.ndarray, save_path: str): + """ + Create a PNG from a 2D or 3-channel slice array using Blender API. + slice_arr expected shape: (W, H) or (W, H, 1) or (W,H,3). Values assumed numeric. + + The function: + - Converts single-channel arrays to RGB by repeating channel. + - Flips the image vertically to match Blender's origin. + - Writes the image file to save_path using a temporary Blender image datablock. + + Parameters: + - array: numpy ndarray representing the slice (W,H,C) or (W,H,1) or (W,H) + - save_path: destination PNG filepath + """ + save_path = Path(save_path) + h, w = array.shape[0], array.shape[1] + if array.shape[2] == 1: + img = np.concatenate([array, array, array], axis=2) + else: + img = array[..., :3] + + # Blender expects pixels as sequence of floats in RGBA, length w*h*4 + # NOTE: Blender's image origin is bottom-left; we'll flip vertically for visual consistency. + img = np.flipud(img) + pixels = np.empty((h * w * 4,), dtype=np.float32) + # fill + pixels[0::4] = img[..., 0].ravel() + pixels[1::4] = img[..., 1].ravel() + pixels[2::4] = img[..., 2].ravel() + pixels[3::4] = 1.0 + + # create a temporary image datablock, save then remove + name = "tmp" + image = bpy.data.images.new( + name, width=w, height=h, alpha=True, float_buffer=False) + image.pixels = pixels.tolist() + # ensure format and filepath then save + image.filepath_raw = str(save_path) + image.file_format = "PNG" + image.save() + # remove from data to avoid clutter + try: + bpy.data.images.remove(image) + except Exception: + pass + + +def split_text_to_lines(text: str, max_chars: int = 60) -> list: + """ + Split `text` into multiple lines not exceeding `max_chars` where possible. + Breaks on whitespace; long words are chunked. + Returns list of line strings. + """ + if not text: + return [] + words = text.split() + lines = [] + cur = "" + for w in words: + if cur: + candidate = cur + " " + w + else: + candidate = w + if len(candidate) <= max_chars: + cur = candidate + else: + if cur: + lines.append(cur) + # if single word longer than max_chars, chunk it + if len(w) > max_chars: + for i in range(0, len(w), max_chars): + lines.append(w[i: i + max_chars]) + cur = "" + else: + cur = w + if cur: + lines.append(cur) + return lines + + +def wrapped_label(layout, text: str, max_chars: int = 42): + """ + Helper to add multiple layout.label calls for a long text. + """ + for line in split_text_to_lines(text, max_chars=max_chars): + layout.label(text=line) + + +def refresh_bioxel_panels(context): + """ + Force Bioxel UI to refresh. + + - Tags relevant areas for redraw. + - Nudges the bioxel_layer_gallery EnumProperty to force items() re-evaluation + when possible (helps template_icon_view pick up updated icons/paths). + Use this from operators after they change b i o x e l _ l a y e r s or previews. + """ + + # tag redraw for all windows/areas + for window in context.window_manager.windows: + for area in window.screen.areas: + # target node editor and other UI areas that may show previews + if area.type in {"NODE_EDITOR", "VIEW_3D", "IMAGE_EDITOR", "OUTLINER"}: + area.tag_redraw() + + +def load_icon(filepath: Path, key: str): + pcoll = PREVIEW_COLLECTIONS["layers"] + + try: + icon = pcoll[key].icon_id + except KeyError: + if filepath.exists(): + pcoll.load(key, str(filepath), "IMAGE") + + try: + icon = pcoll[key].icon_id + except KeyError: + icon = "TEXTURE" + + return icon diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/uv.lock b/uv.lock index 8eab207..58a3050 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.11.0, <3.12" [[package]] @@ -9,26 +9,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycodestyle" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/52/65556a5f917a4b273fd1b705f98687a6bd721dbc45966f0f6687e90a18b0/autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", size = 92064 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/52/65556a5f917a4b273fd1b705f98687a6bd721dbc45966f0f6687e90a18b0/autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", size = 92064, upload-time = "2024-06-23T05:15:55.401Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/9e/f0beffe45b507dca9d7540fad42b316b2fd1076dc484c9b1f23d9da570d7/autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d", size = 45667 }, + { url = "https://files.pythonhosted.org/packages/ad/9e/f0beffe45b507dca9d7540fad42b316b2fd1076dc484c9b1f23d9da570d7/autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d", size = 45667, upload-time = "2024-06-23T05:15:51.29Z" }, ] [[package]] name = "babel" version = "2.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/d2/9671b93d623300f0aef82cde40e25357f11330bdde91743891b22a555bed/babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413", size = 9390000 } +sdist = { url = "https://files.pythonhosted.org/packages/15/d2/9671b93d623300f0aef82cde40e25357f11330bdde91743891b22a555bed/babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413", size = 9390000, upload-time = "2024-05-05T13:54:45.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", size = 9634913 }, + { url = "https://files.pythonhosted.org/packages/27/45/377f7e32a5c93d94cd56542349b34efab5ca3f9e2fd5a68c5e93169aa32d/Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", size = 9634913, upload-time = "2024-05-05T13:54:41.446Z" }, ] [[package]] name = "bioxelnodes" -version = "1.0.7" +version = "1.0.9" source = { editable = "." } dependencies = [ { name = "h5py" }, + { name = "matplotlib" }, { name = "mrcfile" }, { name = "pyometiff" }, { name = "simpleitk" }, @@ -42,6 +43,7 @@ dev = [ { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-material" }, + { name = "pip" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-dependency" }, @@ -51,6 +53,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "h5py", specifier = "==3.11.0" }, + { name = "matplotlib", specifier = "==3.10.7" }, { name = "mrcfile", specifier = "==1.5.1" }, { name = "pyometiff", specifier = "==1.0.0" }, { name = "simpleitk", specifier = "==2.3.1" }, @@ -64,6 +67,7 @@ dev = [ { name = "mike", specifier = ">=2.1.2,<3" }, { name = "mkdocs", specifier = ">=1.6.0,<2" }, { name = "mkdocs-material", specifier = ">=9.5.30,<10" }, + { name = "pip", specifier = ">=25.3" }, { name = "pytest", specifier = ">=8.3.2,<9" }, { name = "pytest-cov", specifier = ">=5.0.0,<6" }, { name = "pytest-dependency", specifier = ">=0.6.0,<0.7" }, @@ -81,19 +85,19 @@ dependencies = [ { name = "zstandard" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/5a/d085d433ad26687d7ba5be235c57679780a3b41316cd7ece1ee65694ebad/bpy-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6e8857c9f483d42f5388642601080ad3e7488970cbc9e59efeedf4a8c26c90b", size = 207901162 }, - { url = "https://files.pythonhosted.org/packages/39/03/579ec8a0d592424bd5743138ff40f2eca1944e427546721cfff091dbe730/bpy-4.2.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4fd0e9872dceb37209d298ccbe77e6c3d0421501ef71b464dde22d09607966ec", size = 220963874 }, - { url = "https://files.pythonhosted.org/packages/0b/bc/5245f98dafa39166a4d4701d64077d281a36110e780011de5c38135089aa/bpy-4.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a8217c2bd7b3424d1f51ce0698c0bf65439de352f7f6ed7b9942e2a9e5eb121b", size = 519384061 }, - { url = "https://files.pythonhosted.org/packages/c4/b0/6f18776208e13acb9bdbf3b5df9db9de42b09ab096aae68b171db422cccb/bpy-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:26ce6210deb8316816938cb1c62c7fd257a8ef38d0c20b9f031a7a47a1492cac", size = 315442011 }, + { url = "https://files.pythonhosted.org/packages/ea/5a/d085d433ad26687d7ba5be235c57679780a3b41316cd7ece1ee65694ebad/bpy-4.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6e8857c9f483d42f5388642601080ad3e7488970cbc9e59efeedf4a8c26c90b", size = 207901162, upload-time = "2024-07-25T09:03:05.781Z" }, + { url = "https://files.pythonhosted.org/packages/39/03/579ec8a0d592424bd5743138ff40f2eca1944e427546721cfff091dbe730/bpy-4.2.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4fd0e9872dceb37209d298ccbe77e6c3d0421501ef71b464dde22d09607966ec", size = 220963874, upload-time = "2024-07-25T09:05:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bc/5245f98dafa39166a4d4701d64077d281a36110e780011de5c38135089aa/bpy-4.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a8217c2bd7b3424d1f51ce0698c0bf65439de352f7f6ed7b9942e2a9e5eb121b", size = 519384061, upload-time = "2024-07-25T09:04:30.48Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b0/6f18776208e13acb9bdbf3b5df9db9de42b09ab096aae68b171db422cccb/bpy-4.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:26ce6210deb8316816938cb1c62c7fd257a8ef38d0c20b9f031a7a47a1492cac", size = 315442011, upload-time = "2024-07-25T09:03:45.29Z" }, ] [[package]] name = "certifi" version = "2024.7.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065, upload-time = "2024-07-04T01:36:11.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, + { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960, upload-time = "2024-07-04T01:36:09.038Z" }, ] [[package]] @@ -103,43 +107,43 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", size = 512873 } +sdist = { url = "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", size = 512873, upload-time = "2023-09-28T18:02:04.656Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/c8/ce05a6cba2bec12d4b28285e66c53cc88dd7385b102dea7231da3b74cfef/cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", size = 182415 }, - { url = "https://files.pythonhosted.org/packages/18/6c/0406611f3d5aadf4c5b08f6c095d874aed8dfc2d3a19892707d72536d5dc/cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", size = 176745 }, - { url = "https://files.pythonhosted.org/packages/58/ac/2a3ea436a6cbaa8f75ddcab39010e5e0817a18f26fef5d2fe2e0c7df3425/cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", size = 443787 }, - { url = "https://files.pythonhosted.org/packages/b5/23/ea84dd4985649fcc179ba3a6c9390412e924d20b0244dc71a6545788f5a2/cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", size = 466550 }, - { url = "https://files.pythonhosted.org/packages/36/44/124481b75d228467950b9e81d20ec963f33517ca551f08956f2838517ece/cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", size = 474224 }, - { url = "https://files.pythonhosted.org/packages/e4/9a/7169ae3a67a7bb9caeb2249f0617ac1458df118305c53afa3dec4a9029cd/cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", size = 457175 }, - { url = "https://files.pythonhosted.org/packages/9b/89/a31c81e36bbb793581d8bba4406a8aac4ba84b2559301c44eef81f4cf5df/cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", size = 464825 }, - { url = "https://files.pythonhosted.org/packages/e0/80/52b71420d68c4be18873318f6735c742f1172bb3b18d23f0306e6444d410/cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", size = 452727 }, - { url = "https://files.pythonhosted.org/packages/47/e3/b6832b1b9a1b6170c585ee2c2d30baf64d0a497c17e6623f42cfeb59c114/cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", size = 476370 }, - { url = "https://files.pythonhosted.org/packages/4a/ac/a4046ab3d72536eff8bc30b39d767f69bd8be715c5e395b71cfca26f03d9/cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", size = 172849 }, - { url = "https://files.pythonhosted.org/packages/5a/c7/694814b3757878b29da39bc2f0cf9d20295f4c1e0a0bde7971708d5f23f8/cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", size = 181495 }, + { url = "https://files.pythonhosted.org/packages/95/c8/ce05a6cba2bec12d4b28285e66c53cc88dd7385b102dea7231da3b74cfef/cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", size = 182415, upload-time = "2023-09-28T18:00:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/18/6c/0406611f3d5aadf4c5b08f6c095d874aed8dfc2d3a19892707d72536d5dc/cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", size = 176745, upload-time = "2023-09-28T18:00:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/2a3ea436a6cbaa8f75ddcab39010e5e0817a18f26fef5d2fe2e0c7df3425/cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", size = 443787, upload-time = "2023-09-28T18:00:50.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/ea84dd4985649fcc179ba3a6c9390412e924d20b0244dc71a6545788f5a2/cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", size = 466550, upload-time = "2023-09-28T18:00:52.874Z" }, + { url = "https://files.pythonhosted.org/packages/36/44/124481b75d228467950b9e81d20ec963f33517ca551f08956f2838517ece/cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", size = 474224, upload-time = "2023-09-28T18:00:54.564Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9a/7169ae3a67a7bb9caeb2249f0617ac1458df118305c53afa3dec4a9029cd/cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", size = 457175, upload-time = "2023-09-28T18:00:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/a31c81e36bbb793581d8bba4406a8aac4ba84b2559301c44eef81f4cf5df/cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", size = 464825, upload-time = "2023-09-28T18:00:57.821Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/52b71420d68c4be18873318f6735c742f1172bb3b18d23f0306e6444d410/cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", size = 452727, upload-time = "2023-09-28T18:00:59.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e3/b6832b1b9a1b6170c585ee2c2d30baf64d0a497c17e6623f42cfeb59c114/cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", size = 476370, upload-time = "2023-09-28T18:01:01.4Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ac/a4046ab3d72536eff8bc30b39d767f69bd8be715c5e395b71cfca26f03d9/cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", size = 172849, upload-time = "2023-09-28T18:01:03.652Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c7/694814b3757878b29da39bc2f0cf9d20295f4c1e0a0bde7971708d5f23f8/cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", size = 181495, upload-time = "2023-09-28T18:01:05.204Z" }, ] [[package]] name = "charset-normalizer" version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, - { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, - { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, - { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, - { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, - { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, - { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, - { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, - { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, - { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, - { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, - { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, - { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, - { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, - { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582, upload-time = "2023-11-01T04:02:59.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645, upload-time = "2023-11-01T04:03:02.186Z" }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398, upload-time = "2023-11-01T04:03:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273, upload-time = "2023-11-01T04:03:05.983Z" }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577, upload-time = "2023-11-01T04:03:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747, upload-time = "2023-11-01T04:03:08.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375, upload-time = "2023-11-01T04:03:10.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474, upload-time = "2023-11-01T04:03:11.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232, upload-time = "2023-11-01T04:03:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859, upload-time = "2023-11-01T04:03:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509, upload-time = "2023-11-01T04:03:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870, upload-time = "2023-11-01T04:03:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" }, ] [[package]] @@ -149,36 +153,63 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "coverage" version = "7.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/c8/a94ce9e17756aed521085ae716d627623374d34f92c1daf7162272ecb030/coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", size = 797590 } +sdist = { url = "https://files.pythonhosted.org/packages/64/c8/a94ce9e17756aed521085ae716d627623374d34f92c1daf7162272ecb030/coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", size = 797590, upload-time = "2024-07-11T17:28:12.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/03/a5140e4f336f0b71023435d7e1564da8e2ff5552e91dfd3ec26424b72c47/coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", size = 205898 }, - { url = "https://files.pythonhosted.org/packages/a2/d3/1f839af6d6cbaee2c50a0bf2d51964b56272e8b94a6504b6ab7c7ecd66c9/coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", size = 206345 }, - { url = "https://files.pythonhosted.org/packages/f9/cf/77b3fb7e132cbae1f112458399157f67f69d5264a9e5a6eae3b91e580b79/coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", size = 238389 }, - { url = "https://files.pythonhosted.org/packages/a8/aa/da317fae6b6057428c72f1023a61d57a79a049f03bc4cbe6ffb7cb61e698/coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", size = 235967 }, - { url = "https://files.pythonhosted.org/packages/39/33/da23d8bfdfdc5c221d2c9a2f169f63a4e04b9a0327c1c67777157155e4d6/coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", size = 237659 }, - { url = "https://files.pythonhosted.org/packages/4d/d7/acea77ab27215da6afff2d5228a64cc4af7db83c481c7b14f47f34cad100/coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", size = 236585 }, - { url = "https://files.pythonhosted.org/packages/e5/b5/fc782807b3e984d0bb0472da8c7aa820ed2827ed95b9fc95c63ccb88f1f9/coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", size = 235238 }, - { url = "https://files.pythonhosted.org/packages/80/88/649d1047bfcbf51227726bfa47073e3b5b94310acbd66f6457c274f74391/coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", size = 236022 }, - { url = "https://files.pythonhosted.org/packages/91/41/eeba3fca0f4b6820184572c50d911959f5d108ec5f8e55fd30f0981d5209/coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", size = 208431 }, - { url = "https://files.pythonhosted.org/packages/54/bb/9f2d3303441e87d27221fc9de4de63994a9570c899726a4f10ac16f79824/coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", size = 209314 }, + { url = "https://files.pythonhosted.org/packages/1b/03/a5140e4f336f0b71023435d7e1564da8e2ff5552e91dfd3ec26424b72c47/coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", size = 205898, upload-time = "2024-07-11T17:26:36.421Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d3/1f839af6d6cbaee2c50a0bf2d51964b56272e8b94a6504b6ab7c7ecd66c9/coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", size = 206345, upload-time = "2024-07-11T17:26:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/77b3fb7e132cbae1f112458399157f67f69d5264a9e5a6eae3b91e580b79/coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", size = 238389, upload-time = "2024-07-11T17:26:40.105Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/da317fae6b6057428c72f1023a61d57a79a049f03bc4cbe6ffb7cb61e698/coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", size = 235967, upload-time = "2024-07-11T17:26:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/da23d8bfdfdc5c221d2c9a2f169f63a4e04b9a0327c1c67777157155e4d6/coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", size = 237659, upload-time = "2024-07-11T17:26:44.066Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/acea77ab27215da6afff2d5228a64cc4af7db83c481c7b14f47f34cad100/coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", size = 236585, upload-time = "2024-07-11T17:26:46.029Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b5/fc782807b3e984d0bb0472da8c7aa820ed2827ed95b9fc95c63ccb88f1f9/coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", size = 235238, upload-time = "2024-07-11T17:26:47.745Z" }, + { url = "https://files.pythonhosted.org/packages/80/88/649d1047bfcbf51227726bfa47073e3b5b94310acbd66f6457c274f74391/coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", size = 236022, upload-time = "2024-07-11T17:26:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/91/41/eeba3fca0f4b6820184572c50d911959f5d108ec5f8e55fd30f0981d5209/coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", size = 208431, upload-time = "2024-07-11T17:26:52.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/9f2d3303441e87d27221fc9de4de63994a9570c899726a4f10ac16f79824/coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", size = 209314, upload-time = "2024-07-11T17:26:54.278Z" }, ] [package.optional-dependencies] @@ -186,21 +217,47 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "cython" version = "3.0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/f7/2fdd9205a2eedee7d9b0abbf15944a1151eb943001dbdc5233b1d1cfc34e/Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99", size = 2751764 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/f7/2fdd9205a2eedee7d9b0abbf15944a1151eb943001dbdc5233b1d1cfc34e/Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99", size = 2751764, upload-time = "2024-03-30T20:12:49.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/69/198e26fb362a35460cbc72596352228fb8bddb15d11a787acfd7fa30aec4/Cython-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:051069638abfb076900b0c2bcb6facf545655b3f429e80dd14365192074af5a4", size = 3120551 }, - { url = "https://files.pythonhosted.org/packages/8e/30/947edd67f2ca21da1b88cba9c7f24fd27923a068138e0077095927dbee9e/Cython-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712760879600907189c7d0d346851525545484e13cd8b787e94bfd293da8ccf0", size = 3441040 }, - { url = "https://files.pythonhosted.org/packages/45/82/077c13035d6f45d8b8b74d67e9f73f2bfc54ef8d1f79572790f6f7d2b4f5/Cython-3.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38d40fa1324ac47c04483d151f5e092406a147eac88a18aec789cf01c089c3f2", size = 3596464 }, - { url = "https://files.pythonhosted.org/packages/a2/d5/54f108e43dcc94ddcd227ee258700165bf3e447f6b0fac316b5d822fb882/Cython-3.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bd49a3a9fdff65446a3e1c2bfc0ec85c6ce4c3cad27cd4ad7ba150a62b7fb59", size = 3640507 }, - { url = "https://files.pythonhosted.org/packages/1d/bb/0a8451b86144be96132531b4b868640c61744655dc92ed08d924ecaf8931/Cython-3.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8df79b596633b8295eaa48b1157d796775c2bb078f32267d32f3001b687f2fd", size = 3462024 }, - { url = "https://files.pythonhosted.org/packages/44/7f/8262019f0076f5e79ce4de855d8601e90b66fb44925c161b1c9670a21889/Cython-3.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bcc9795990e525c192bc5c0775e441d7d56d7a7d02210451e9e13c0448dba51b", size = 3618064 }, - { url = "https://files.pythonhosted.org/packages/8d/11/01847d11faa22d058ceb441e3185364e13342b276ce25026118d0779ff5e/Cython-3.0.10-cp311-cp311-win32.whl", hash = "sha256:09f2000041db482cad3bfce94e1fa3a4c82b0e57390a164c02566cbbda8c4f12", size = 2577020 }, - { url = "https://files.pythonhosted.org/packages/18/ec/f47a721071d084d6c2b6783eb8d058b964b1450cb708d920d0d792f42001/Cython-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:3919a55ec9b6c7db6f68a004c21c05ed540c40dbe459ced5d801d5a1f326a053", size = 2788624 }, - { url = "https://files.pythonhosted.org/packages/b6/83/b0a63fc7b315edd46821a1a381d18765c1353d201246da44558175cddd56/Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2", size = 1170278 }, + { url = "https://files.pythonhosted.org/packages/05/69/198e26fb362a35460cbc72596352228fb8bddb15d11a787acfd7fa30aec4/Cython-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:051069638abfb076900b0c2bcb6facf545655b3f429e80dd14365192074af5a4", size = 3120551, upload-time = "2024-03-30T20:31:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/947edd67f2ca21da1b88cba9c7f24fd27923a068138e0077095927dbee9e/Cython-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712760879600907189c7d0d346851525545484e13cd8b787e94bfd293da8ccf0", size = 3441040, upload-time = "2024-03-30T20:13:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/077c13035d6f45d8b8b74d67e9f73f2bfc54ef8d1f79572790f6f7d2b4f5/Cython-3.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38d40fa1324ac47c04483d151f5e092406a147eac88a18aec789cf01c089c3f2", size = 3596464, upload-time = "2024-03-30T20:14:03.659Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/54f108e43dcc94ddcd227ee258700165bf3e447f6b0fac316b5d822fb882/Cython-3.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bd49a3a9fdff65446a3e1c2bfc0ec85c6ce4c3cad27cd4ad7ba150a62b7fb59", size = 3640507, upload-time = "2024-03-30T20:14:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bb/0a8451b86144be96132531b4b868640c61744655dc92ed08d924ecaf8931/Cython-3.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8df79b596633b8295eaa48b1157d796775c2bb078f32267d32f3001b687f2fd", size = 3462024, upload-time = "2024-03-30T20:14:10.886Z" }, + { url = "https://files.pythonhosted.org/packages/44/7f/8262019f0076f5e79ce4de855d8601e90b66fb44925c161b1c9670a21889/Cython-3.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bcc9795990e525c192bc5c0775e441d7d56d7a7d02210451e9e13c0448dba51b", size = 3618064, upload-time = "2024-03-30T20:14:14.326Z" }, + { url = "https://files.pythonhosted.org/packages/8d/11/01847d11faa22d058ceb441e3185364e13342b276ce25026118d0779ff5e/Cython-3.0.10-cp311-cp311-win32.whl", hash = "sha256:09f2000041db482cad3bfce94e1fa3a4c82b0e57390a164c02566cbbda8c4f12", size = 2577020, upload-time = "2024-03-30T20:14:16.725Z" }, + { url = "https://files.pythonhosted.org/packages/18/ec/f47a721071d084d6c2b6783eb8d058b964b1450cb708d920d0d792f42001/Cython-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:3919a55ec9b6c7db6f68a004c21c05ed540c40dbe459ced5d801d5a1f326a053", size = 2788624, upload-time = "2024-03-30T20:14:19.5Z" }, + { url = "https://files.pythonhosted.org/packages/b6/83/b0a63fc7b315edd46821a1a381d18765c1353d201246da44558175cddd56/Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2", size = 1170278, upload-time = "2024-03-30T20:12:43.74Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, ] [[package]] @@ -210,9 +267,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] @@ -222,21 +279,21 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/8f/e557819155a282da36fb21f8de4730cfd10a964b52b3ae8d20157ac1c668/h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9", size = 406519 } +sdist = { url = "https://files.pythonhosted.org/packages/52/8f/e557819155a282da36fb21f8de4730cfd10a964b52b3ae8d20157ac1c668/h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9", size = 406519, upload-time = "2024-04-10T10:52:39.585Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/52/38bb74cc4362738cc7ef819503fc54d70f0c3a7378519ccb0ac309389122/h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba", size = 3489913 }, - { url = "https://files.pythonhosted.org/packages/f0/af/dfbea0c69fe725e9e77259d42f4e14eb582eb094200aaf697feb36f513d8/h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007", size = 2946912 }, - { url = "https://files.pythonhosted.org/packages/af/26/f231ee425c8df93c1abbead3d90ea4a5ff3d6aa49e0edfd3b4c017e74844/h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3", size = 5420165 }, - { url = "https://files.pythonhosted.org/packages/d8/5e/b7b83cfe60504cc4d24746aed04353af7ea8ec104e597e5ae71b8d0390cb/h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e", size = 2979079 }, + { url = "https://files.pythonhosted.org/packages/a0/52/38bb74cc4362738cc7ef819503fc54d70f0c3a7378519ccb0ac309389122/h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba", size = 3489913, upload-time = "2024-04-10T10:49:15.92Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/dfbea0c69fe725e9e77259d42f4e14eb582eb094200aaf697feb36f513d8/h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007", size = 2946912, upload-time = "2024-04-10T10:49:25.757Z" }, + { url = "https://files.pythonhosted.org/packages/af/26/f231ee425c8df93c1abbead3d90ea4a5ff3d6aa49e0edfd3b4c017e74844/h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3", size = 5420165, upload-time = "2024-04-10T10:49:57.203Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/b7b83cfe60504cc4d24746aed04353af7ea8ec104e597e5ae71b8d0390cb/h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e", size = 2979079, upload-time = "2024-04-10T10:50:11.4Z" }, ] [[package]] name = "idna" version = "3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575, upload-time = "2024-04-11T03:34:43.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836, upload-time = "2024-04-11T03:34:41.447Z" }, ] [[package]] @@ -246,15 +303,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/cb/62e31d1367baded4bfd0bdf5c8e57da06db81ef4e472080ca5306e5588f2/imagecodecs-2024.6.1.tar.gz", hash = "sha256:0f3e94b7f51e2f78287b7ffae82cd850b1007639148894538274fa50bd179886", size = 9459867 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/cb/62e31d1367baded4bfd0bdf5c8e57da06db81ef4e472080ca5306e5588f2/imagecodecs-2024.6.1.tar.gz", hash = "sha256:0f3e94b7f51e2f78287b7ffae82cd850b1007639148894538274fa50bd179886", size = 9459867, upload-time = "2024-06-02T05:48:03.255Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/7d/f891de3b967c6928da33ce660f982865caccc25532f4a68d0dd7ab7bbee3/imagecodecs-2024.6.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:328ea133e0f292cf54c5feb13e247fbf45a6055c8dc6822e841c208d2dc5c96a", size = 15112249 }, - { url = "https://files.pythonhosted.org/packages/7c/49/ef1569a1b4994444e789be74a689f9f933928cf09ed7d58bcba3d58693a1/imagecodecs-2024.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8045ea3a9c9de78ea00e2a387f47d784434bfad05967decbe0c1b3bee5aadf25", size = 12485242 }, - { url = "https://files.pythonhosted.org/packages/e2/dd/3abc85097d0596fc0c6e90b18fca8a46cb60e70ebb62016718c66c588af7/imagecodecs-2024.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42bd9ec14e4d38f15e2fa387c90b726dba42c16da0a9b6ff2c23e01478b8cd93", size = 40346211 }, - { url = "https://files.pythonhosted.org/packages/24/bc/37c6949cf5234098ee6e733b648f2dfbc92fc115262aba38d0ebd2e78570/imagecodecs-2024.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eb5b2d755a64de9a7e0604d5dcc1151c96b43b4e5ac69bebc6d8d790b77ca58", size = 41580948 }, - { url = "https://files.pythonhosted.org/packages/e2/45/05962232ec0878e7f0a4605b7afd283d13346f60a08675acdeaea0719cf4/imagecodecs-2024.6.1-cp311-cp311-win32.whl", hash = "sha256:03ace438a843e024239cddbe7fe6940bd2a6cf3316b08c281b95842b5217c0f7", size = 21376522 }, - { url = "https://files.pythonhosted.org/packages/08/a4/6e8cc00146d89fd25d82090d1ec8fae6ca7530500c0bf163f1eb400ee572/imagecodecs-2024.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd926589c6e3c564490b93258b1a2ca3b040da10c21e99b618b7be6dd76b2a25", size = 25725984 }, - { url = "https://files.pythonhosted.org/packages/a4/99/0b0a5ce0d6a05dd25ca9df608b481af35a4ad5aa0259078a495ddd5f159b/imagecodecs-2024.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:101fcef57aedb8730d1d2d1779dfbaa23daf7e50cd4130e88945a4fe34d0212f", size = 20803413 }, + { url = "https://files.pythonhosted.org/packages/21/7d/f891de3b967c6928da33ce660f982865caccc25532f4a68d0dd7ab7bbee3/imagecodecs-2024.6.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:328ea133e0f292cf54c5feb13e247fbf45a6055c8dc6822e841c208d2dc5c96a", size = 15112249, upload-time = "2024-06-02T06:30:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/7c/49/ef1569a1b4994444e789be74a689f9f933928cf09ed7d58bcba3d58693a1/imagecodecs-2024.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8045ea3a9c9de78ea00e2a387f47d784434bfad05967decbe0c1b3bee5aadf25", size = 12485242, upload-time = "2024-06-02T05:46:04.922Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dd/3abc85097d0596fc0c6e90b18fca8a46cb60e70ebb62016718c66c588af7/imagecodecs-2024.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42bd9ec14e4d38f15e2fa387c90b726dba42c16da0a9b6ff2c23e01478b8cd93", size = 40346211, upload-time = "2024-06-02T05:46:13.237Z" }, + { url = "https://files.pythonhosted.org/packages/24/bc/37c6949cf5234098ee6e733b648f2dfbc92fc115262aba38d0ebd2e78570/imagecodecs-2024.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eb5b2d755a64de9a7e0604d5dcc1151c96b43b4e5ac69bebc6d8d790b77ca58", size = 41580948, upload-time = "2024-06-02T05:46:22.602Z" }, + { url = "https://files.pythonhosted.org/packages/e2/45/05962232ec0878e7f0a4605b7afd283d13346f60a08675acdeaea0719cf4/imagecodecs-2024.6.1-cp311-cp311-win32.whl", hash = "sha256:03ace438a843e024239cddbe7fe6940bd2a6cf3316b08c281b95842b5217c0f7", size = 21376522, upload-time = "2024-06-02T05:46:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/6e8cc00146d89fd25d82090d1ec8fae6ca7530500c0bf163f1eb400ee572/imagecodecs-2024.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd926589c6e3c564490b93258b1a2ca3b040da10c21e99b618b7be6dd76b2a25", size = 25725984, upload-time = "2024-06-02T05:46:34.468Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/0b0a5ce0d6a05dd25ca9df608b481af35a4ad5aa0259078a495ddd5f159b/imagecodecs-2024.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:101fcef57aedb8730d1d2d1779dfbaa23daf7e50cd4130e88945a4fe34d0212f", size = 20803413, upload-time = "2024-06-02T05:46:41.326Z" }, ] [[package]] @@ -264,27 +321,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/a1/db39a513aa99ab3442010a994eef1cb977a436aded53042e69bee6959f74/importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d", size = 53907 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a1/db39a513aa99ab3442010a994eef1cb977a436aded53042e69bee6959f74/importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d", size = 53907, upload-time = "2024-07-24T15:22:17.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/47/bb25ec04985d0693da478797c3d8c1092b140f3a53ccb984fbbd38affa5b/importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369", size = 25920 }, + { url = "https://files.pythonhosted.org/packages/82/47/bb25ec04985d0693da478797c3d8c1092b140f3a53ccb984fbbd38affa5b/importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369", size = 25920, upload-time = "2024-07-24T15:22:15.491Z" }, ] [[package]] name = "importlib-resources" version = "6.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/6ee73859d6be81c6ea7ebac89655e92740296419bd37e5c8abdb5b62fd55/importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145", size = 42040 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/6ee73859d6be81c6ea7ebac89655e92740296419bd37e5c8abdb5b62fd55/importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145", size = 42040, upload-time = "2024-03-21T13:42:34.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/06/4df55e1b7b112d183f65db9503bff189e97179b256e1ea450a3c365241e0/importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c", size = 38168 }, + { url = "https://files.pythonhosted.org/packages/75/06/4df55e1b7b112d183f65db9503bff189e97179b256e1ea450a3c365241e0/importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c", size = 38168, upload-time = "2024-03-21T13:42:33.243Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] [[package]] @@ -294,74 +351,129 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245, upload-time = "2024-05-05T23:42:02.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271, upload-time = "2024-05-05T23:41:59.928Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] name = "lxml" version = "5.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/f7/ffbb6d2eb67b80a45b8a0834baa5557a14a5ffce0979439e7cd7f0c4055b/lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87", size = 3678631 } +sdist = { url = "https://files.pythonhosted.org/packages/63/f7/ffbb6d2eb67b80a45b8a0834baa5557a14a5ffce0979439e7cd7f0c4055b/lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87", size = 3678631, upload-time = "2024-05-13T05:58:21.968Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/6a/24e9f77d17668dd4ac0a6c2a56113fd3e0db07cee51e3a67afcd47c597e5/lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545", size = 8137962 }, - { url = "https://files.pythonhosted.org/packages/4e/42/3bfe92749715c819763d2205370ecc7f586b44e277f38839e27cce7d6bb8/lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88", size = 4424403 }, - { url = "https://files.pythonhosted.org/packages/d5/fd/4899215277e3ef1767019fab178fad8a149081f80cf886a73dc0ba1badae/lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083", size = 5099309 }, - { url = "https://files.pythonhosted.org/packages/15/3d/d84d07fc450af34ce49b88a5aac805b486f38c9f9305fba647a39367c51c/lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1", size = 4810147 }, - { url = "https://files.pythonhosted.org/packages/bc/c6/32af0ec3e8323e12212c064f924ddf993017e68d1f50e03da2a3be1289c1/lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734", size = 5406165 }, - { url = "https://files.pythonhosted.org/packages/67/c7/6060ea3efbd1a60a10963b1b09f493fc8f6f6728310a7a77479a3f9ab20a/lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f", size = 4866924 }, - { url = "https://files.pythonhosted.org/packages/8a/f7/f5df71c70c4d14d186dd86fd0e9ebeffdb63b9b86fb19fe9315f9576266b/lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed", size = 4967116 }, - { url = "https://files.pythonhosted.org/packages/4e/56/c35969591789763657eb16c2fa79c924823b97da5536da8b89e11582da89/lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3", size = 4811365 }, - { url = "https://files.pythonhosted.org/packages/e7/28/1809a5406282c03df561a3c8143c143bd515d5668f1a138f51aec6d2618e/lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", size = 5452505 }, - { url = "https://files.pythonhosted.org/packages/99/a1/d91217a8d7fef5ac41af87c916d322c273a9b2047c735ea1dafa2ac46d16/lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d", size = 4973479 }, - { url = "https://files.pythonhosted.org/packages/ad/b7/0dc82afed00c4c189cfd0b83464f9a431c66de8e73d911063956a147276a/lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5", size = 5013920 }, - { url = "https://files.pythonhosted.org/packages/5f/e0/4cd021850f2e8ab5ce6ce294556300bd4b5c1eb7def88b5f859449dc883c/lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", size = 4849156 }, - { url = "https://files.pythonhosted.org/packages/f0/f4/fb01451fda1e121eb8f117a00040454ca05a9c9d82b308272042eebd05f3/lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115", size = 5408551 }, - { url = "https://files.pythonhosted.org/packages/2f/ca/0376418e202e9423d47f86ae09db885fa6e109d0efb602f6468e6d1e8e77/lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04", size = 4829966 }, - { url = "https://files.pythonhosted.org/packages/74/c4/4e6f5e2be18f8eb76dff5bff3619c2c654650fee93aea37a92866efe90bc/lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad", size = 4976003 }, - { url = "https://files.pythonhosted.org/packages/3b/ca/5d74a0572c911f8dbf12d89abe0fdcbe0409c18978b5694392becd4d56fb/lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8", size = 4852709 }, - { url = "https://files.pythonhosted.org/packages/83/07/d95e9663ad8d875f344930e4fb52a0a6f56225267d3cc6e3e9bfa44ca736/lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5", size = 5479261 }, - { url = "https://files.pythonhosted.org/packages/df/e1/9ebae8d05492a8e9f632f2add15199e7bca5c1b063444e360a7bde685718/lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", size = 4944199 }, - { url = "https://files.pythonhosted.org/packages/ec/ab/189f571450e3393d4d442f88683d11b5a47c88e66a4e6b0d467500360483/lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b", size = 5033723 }, - { url = "https://files.pythonhosted.org/packages/80/d7/f28ccad4f08596592a58ad945c636140268bb4de9dcf4d10c9f72108d8a5/lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438", size = 3475657 }, - { url = "https://files.pythonhosted.org/packages/04/19/d6aa2d980f220a04c91d4de538d2fea1a65535e7b0a4aec0998ce46e3667/lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be", size = 3816665 }, + { url = "https://files.pythonhosted.org/packages/da/6a/24e9f77d17668dd4ac0a6c2a56113fd3e0db07cee51e3a67afcd47c597e5/lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545", size = 8137962, upload-time = "2024-05-13T05:08:37.117Z" }, + { url = "https://files.pythonhosted.org/packages/4e/42/3bfe92749715c819763d2205370ecc7f586b44e277f38839e27cce7d6bb8/lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88", size = 4424403, upload-time = "2024-05-13T05:09:02.004Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fd/4899215277e3ef1767019fab178fad8a149081f80cf886a73dc0ba1badae/lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083", size = 5099309, upload-time = "2024-05-13T05:09:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/15/3d/d84d07fc450af34ce49b88a5aac805b486f38c9f9305fba647a39367c51c/lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1", size = 4810147, upload-time = "2024-05-13T05:09:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c6/32af0ec3e8323e12212c064f924ddf993017e68d1f50e03da2a3be1289c1/lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734", size = 5406165, upload-time = "2024-05-13T05:10:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/67/c7/6060ea3efbd1a60a10963b1b09f493fc8f6f6728310a7a77479a3f9ab20a/lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f", size = 4866924, upload-time = "2024-05-13T05:10:49.944Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f7/f5df71c70c4d14d186dd86fd0e9ebeffdb63b9b86fb19fe9315f9576266b/lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed", size = 4967116, upload-time = "2024-05-13T05:11:19.005Z" }, + { url = "https://files.pythonhosted.org/packages/4e/56/c35969591789763657eb16c2fa79c924823b97da5536da8b89e11582da89/lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3", size = 4811365, upload-time = "2024-05-13T05:11:46.543Z" }, + { url = "https://files.pythonhosted.org/packages/e7/28/1809a5406282c03df561a3c8143c143bd515d5668f1a138f51aec6d2618e/lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", size = 5452505, upload-time = "2024-05-13T05:12:14.507Z" }, + { url = "https://files.pythonhosted.org/packages/99/a1/d91217a8d7fef5ac41af87c916d322c273a9b2047c735ea1dafa2ac46d16/lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d", size = 4973479, upload-time = "2024-05-13T05:12:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b7/0dc82afed00c4c189cfd0b83464f9a431c66de8e73d911063956a147276a/lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5", size = 5013920, upload-time = "2024-05-13T05:13:10.517Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e0/4cd021850f2e8ab5ce6ce294556300bd4b5c1eb7def88b5f859449dc883c/lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", size = 4849156, upload-time = "2024-05-13T05:13:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/fb01451fda1e121eb8f117a00040454ca05a9c9d82b308272042eebd05f3/lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115", size = 5408551, upload-time = "2024-05-13T05:14:08.124Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ca/0376418e202e9423d47f86ae09db885fa6e109d0efb602f6468e6d1e8e77/lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04", size = 4829966, upload-time = "2024-05-13T05:14:39.48Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/4e6f5e2be18f8eb76dff5bff3619c2c654650fee93aea37a92866efe90bc/lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad", size = 4976003, upload-time = "2024-05-13T05:15:02.719Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ca/5d74a0572c911f8dbf12d89abe0fdcbe0409c18978b5694392becd4d56fb/lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8", size = 4852709, upload-time = "2024-05-13T05:15:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/83/07/d95e9663ad8d875f344930e4fb52a0a6f56225267d3cc6e3e9bfa44ca736/lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5", size = 5479261, upload-time = "2024-05-13T05:15:56.219Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/9ebae8d05492a8e9f632f2add15199e7bca5c1b063444e360a7bde685718/lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", size = 4944199, upload-time = "2024-05-13T05:16:25.616Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/189f571450e3393d4d442f88683d11b5a47c88e66a4e6b0d467500360483/lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b", size = 5033723, upload-time = "2024-05-13T05:16:49.077Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/f28ccad4f08596592a58ad945c636140268bb4de9dcf4d10c9f72108d8a5/lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438", size = 3475657, upload-time = "2024-05-13T05:17:03.477Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/d6aa2d980f220a04c91d4de538d2fea1a65535e7b0a4aec0998ce46e3667/lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be", size = 3816665, upload-time = "2024-05-13T05:17:24.698Z" }, ] [[package]] name = "markdown" version = "3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/02/4785861427848cc11e452cc62bb541006a1087cf04a1de83aedd5530b948/Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224", size = 354715 } +sdist = { url = "https://files.pythonhosted.org/packages/22/02/4785861427848cc11e452cc62bb541006a1087cf04a1de83aedd5530b948/Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224", size = 354715, upload-time = "2024-03-14T15:37:59.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b3/0c0c994fe49cd661084f8d5dc06562af53818cc0abefaca35bdc894577c3/Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f", size = 105381 }, + { url = "https://files.pythonhosted.org/packages/fc/b3/0c0c994fe49cd661084f8d5dc06562af53818cc0abefaca35bdc894577c3/Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f", size = 105381, upload-time = "2024-03-14T15:37:57.457Z" }, ] [[package]] name = "markupsafe" version = "2.1.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, - { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, - { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, - { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, - { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, - { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, - { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, - { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, - { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, - { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] @@ -378,9 +490,9 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "verspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/03/14135a1dc0266ecdbed3b9545c34b5268eeca74f8f0e16b72d0d4468a494/mike-2.1.2.tar.gz", hash = "sha256:d59cc8054c50f9c8a046cfd47f9b700cf9ff1b2b19f420bd8812ca6f94fa8bd3", size = 38085 } +sdist = { url = "https://files.pythonhosted.org/packages/df/03/14135a1dc0266ecdbed3b9545c34b5268eeca74f8f0e16b72d0d4468a494/mike-2.1.2.tar.gz", hash = "sha256:d59cc8054c50f9c8a046cfd47f9b700cf9ff1b2b19f420bd8812ca6f94fa8bd3", size = 38085, upload-time = "2024-06-24T16:35:59.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/73/fd9536c4609dcf3a1fec5f7c49b2a48fb9020e17e6dd731d0a1144aaf42a/mike-2.1.2-py3-none-any.whl", hash = "sha256:d61d9b423ab412d634ca2bd520136d5114e3cc73f4bbd1aa6a0c6625c04918c0", size = 33760 }, + { url = "https://files.pythonhosted.org/packages/4e/73/fd9536c4609dcf3a1fec5f7c49b2a48fb9020e17e6dd731d0a1144aaf42a/mike-2.1.2-py3-none-any.whl", hash = "sha256:d61d9b423ab412d634ca2bd520136d5114e3cc73f4bbd1aa6a0c6625c04918c0", size = 33760, upload-time = "2024-06-24T16:35:57.294Z" }, ] [[package]] @@ -402,9 +514,9 @@ dependencies = [ { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/6b/26b33cc8ad54e8bc0345cddc061c2c5c23e364de0ecd97969df23f95a673/mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512", size = 3888392 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/6b/26b33cc8ad54e8bc0345cddc061c2c5c23e364de0ecd97969df23f95a673/mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512", size = 3888392, upload-time = "2024-04-20T17:55:45.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/c0/930dcf5a3e96b9c8e7ad15502603fc61d495479699e2d2c381e3d37294d1/mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7", size = 3862264 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/930dcf5a3e96b9c8e7ad15502603fc61d495479699e2d2c381e3d37294d1/mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7", size = 3862264, upload-time = "2024-04-20T17:55:42.126Z" }, ] [[package]] @@ -416,9 +528,9 @@ dependencies = [ { name = "platformdirs" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] @@ -438,18 +550,18 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/bd/00ada20787910d8ca0def5aeec7449849dbf6294c529c1e596df4a6457eb/mkdocs_material-9.5.30.tar.gz", hash = "sha256:3fd417dd42d679e3ba08b9e2d72cd8b8af142cc4a3969676ad6b00993dd182ec", size = 4103479 } +sdist = { url = "https://files.pythonhosted.org/packages/a4/bd/00ada20787910d8ca0def5aeec7449849dbf6294c529c1e596df4a6457eb/mkdocs_material-9.5.30.tar.gz", hash = "sha256:3fd417dd42d679e3ba08b9e2d72cd8b8af142cc4a3969676ad6b00993dd182ec", size = 4103479, upload-time = "2024-07-23T07:14:02.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/b4/fe7fd093d6560ee756f8ab41d5cc50b362ac85f7f0c0e00ee82fadbc4f6d/mkdocs_material-9.5.30-py3-none-any.whl", hash = "sha256:fc070689c5250a180e9b9d79d8491ef9a3a7acb240db0728728d6c31eeb131d4", size = 8817826 }, + { url = "https://files.pythonhosted.org/packages/f3/b4/fe7fd093d6560ee756f8ab41d5cc50b362ac85f7f0c0e00ee82fadbc4f6d/mkdocs_material-9.5.30-py3-none-any.whl", hash = "sha256:fc070689c5250a180e9b9d79d8491ef9a3a7acb240db0728728d6c31eeb131d4", size = 8817826, upload-time = "2024-07-23T07:13:57.765Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] @@ -459,96 +571,131 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/28/0f3a0e0c8c6998a93598873ae2624baf6862c68cf4df955d78c1f4ca38c6/mrcfile-1.5.1.tar.gz", hash = "sha256:403c4bb0ac842410ce5ea501f4fddc91ea37c12ef869d508d3ac571868d82ac2", size = 56558 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/28/0f3a0e0c8c6998a93598873ae2624baf6862c68cf4df955d78c1f4ca38c6/mrcfile-1.5.1.tar.gz", hash = "sha256:403c4bb0ac842410ce5ea501f4fddc91ea37c12ef869d508d3ac571868d82ac2", size = 56558, upload-time = "2024-07-11T11:36:10.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/f0/c791571925cd21db9488a08515dd3b86fa56bcc51be396865a9fee46f8bc/mrcfile-1.5.1-py2.py3-none-any.whl", hash = "sha256:06900f1245e66dd4617cbd4a7117a2d75d53fc4e5b74d811766f71a858b059a9", size = 44224 }, + { url = "https://files.pythonhosted.org/packages/50/f0/c791571925cd21db9488a08515dd3b86fa56bcc51be396865a9fee46f8bc/mrcfile-1.5.1-py2.py3-none-any.whl", hash = "sha256:06900f1245e66dd4617cbd4a7117a2d75d53fc4e5b74d811766f71a858b059a9", size = 44224, upload-time = "2024-07-11T11:36:08.595Z" }, ] [[package]] name = "numpy" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/8a/0db635b225d2aa2984e405dc14bd2b0c324a0c312ea1bc9d283f2b83b038/numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3", size = 18872007 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/8a/0db635b225d2aa2984e405dc14bd2b0c324a0c312ea1bc9d283f2b83b038/numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3", size = 18872007, upload-time = "2024-07-21T13:49:20.698Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/d6/ff66f4f87518a435538e15cc9e0477a88398512a18783e748914f0daf5ea/numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268", size = 21238269 }, - { url = "https://files.pythonhosted.org/packages/c5/64/853cfc37494471e64ea9f7bf3bc3b4bb39450e6db5beeb05e2a66beef612/numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e", size = 13334324 }, - { url = "https://files.pythonhosted.org/packages/d1/d8/597b4b2e396a77cbec677c9de33bb1789d5c3b66d653cb723d00eb331e99/numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343", size = 5298376 }, - { url = "https://files.pythonhosted.org/packages/64/58/8664ff3747ac719ae1a5b9c0020533435158180a27f2f88a2b7a253bb623/numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b", size = 6903817 }, - { url = "https://files.pythonhosted.org/packages/72/44/71ac0090d4ccb512fcac0ef0e5208248423a1ce30381541700470ac09b75/numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe", size = 13916254 }, - { url = "https://files.pythonhosted.org/packages/ef/27/39622993e8688a1f05898a3c3b2836b856f79c06637ebd4b71cb35cc9b18/numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67", size = 19540260 }, - { url = "https://files.pythonhosted.org/packages/6a/26/a32b5a6b3f090860aeefb3619bfea09f717d73908bd65e69e8ab0cac9c07/numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7", size = 19938602 }, - { url = "https://files.pythonhosted.org/packages/34/b6/a88a9953d0be231c67aa0b3714d6138507490753beaa927f0b33f20cdca2/numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55", size = 14425300 }, - { url = "https://files.pythonhosted.org/packages/e4/e2/e763e102bea9c188b43ea144a91c22bec669736889a6e0be0235d64666d7/numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4", size = 6467652 }, - { url = "https://files.pythonhosted.org/packages/3d/67/928e8f0d5c7fd32f32fb5caf92b186a1b3826dbaf5a294e13a976d6c38b6/numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8", size = 16559280 }, + { url = "https://files.pythonhosted.org/packages/29/d6/ff66f4f87518a435538e15cc9e0477a88398512a18783e748914f0daf5ea/numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268", size = 21238269, upload-time = "2024-07-21T13:33:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/c5/64/853cfc37494471e64ea9f7bf3bc3b4bb39450e6db5beeb05e2a66beef612/numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e", size = 13334324, upload-time = "2024-07-21T13:33:51.455Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d8/597b4b2e396a77cbec677c9de33bb1789d5c3b66d653cb723d00eb331e99/numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343", size = 5298376, upload-time = "2024-07-21T13:34:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/64/58/8664ff3747ac719ae1a5b9c0020533435158180a27f2f88a2b7a253bb623/numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b", size = 6903817, upload-time = "2024-07-21T13:34:13.878Z" }, + { url = "https://files.pythonhosted.org/packages/72/44/71ac0090d4ccb512fcac0ef0e5208248423a1ce30381541700470ac09b75/numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe", size = 13916254, upload-time = "2024-07-21T13:34:35.466Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/39622993e8688a1f05898a3c3b2836b856f79c06637ebd4b71cb35cc9b18/numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67", size = 19540260, upload-time = "2024-07-21T13:35:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/6a/26/a32b5a6b3f090860aeefb3619bfea09f717d73908bd65e69e8ab0cac9c07/numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7", size = 19938602, upload-time = "2024-07-21T13:35:38.996Z" }, + { url = "https://files.pythonhosted.org/packages/34/b6/a88a9953d0be231c67aa0b3714d6138507490753beaa927f0b33f20cdca2/numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55", size = 14425300, upload-time = "2024-07-21T13:36:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/e763e102bea9c188b43ea144a91c22bec669736889a6e0be0235d64666d7/numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4", size = 6467652, upload-time = "2024-07-21T13:36:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/928e8f0d5c7fd32f32fb5caf92b186a1b3826dbaf5a294e13a976d6c38b6/numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8", size = 16559280, upload-time = "2024-07-21T13:36:44.641Z" }, ] [[package]] name = "packaging" version = "24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, ] [[package]] name = "paginate" version = "0.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/58/e670a947136fdcece8ac5376b3df1369d29e4f6659b0c9b358605b115e9e/paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d", size = 12840 } +sdist = { url = "https://files.pythonhosted.org/packages/68/58/e670a947136fdcece8ac5376b3df1369d29e4f6659b0c9b358605b115e9e/paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d", size = 12840, upload-time = "2016-11-22T12:54:04.503Z" } [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, ] [[package]] name = "platformdirs" version = "4.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916, upload-time = "2024-05-15T03:18:23.372Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146 }, + { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146, upload-time = "2024-05-15T03:18:21.209Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pycodestyle" version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/56/52d8283e1a1c85695291040192776931782831e21117c84311cbdd63f70c/pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", size = 39055 } +sdist = { url = "https://files.pythonhosted.org/packages/10/56/52d8283e1a1c85695291040192776931782831e21117c84311cbdd63f70c/pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", size = 39055, upload-time = "2024-06-15T21:28:55.576Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/c4/bf8ede2d1641e0a2e027c6d0c7060e00332851ea772cc5cee42a4a207707/pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4", size = 31221 }, + { url = "https://files.pythonhosted.org/packages/55/c4/bf8ede2d1641e0a2e027c6d0c7060e00332851ea772cc5cee42a4a207707/pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4", size = 31221, upload-time = "2024-06-15T21:28:54.101Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pygments" version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905, upload-time = "2024-05-04T13:42:02.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513, upload-time = "2024-05-04T13:41:57.345Z" }, ] [[package]] @@ -559,9 +706,9 @@ dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/d3/fb86beeaa4416f73a28a5e8d440976b7cada2b2d7b5e715b2bd849d4de32/pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753", size = 812128 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/d3/fb86beeaa4416f73a28a5e8d440976b7cada2b2d7b5e715b2bd849d4de32/pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753", size = 812128, upload-time = "2024-07-27T19:13:08.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/41/18b5dc5e97ec3ff1c2f51d372e570a9fbe231f1124dcc36dbc6b47f93058/pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626", size = 250954 }, + { url = "https://files.pythonhosted.org/packages/7b/41/18b5dc5e97ec3ff1c2f51d372e570a9fbe231f1124dcc36dbc6b47f93058/pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626", size = 250954, upload-time = "2024-07-27T19:13:06.009Z" }, ] [[package]] @@ -574,18 +721,18 @@ dependencies = [ { name = "numpy" }, { name = "tifffile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/77/7e8d33d76d0bd6091f88a3c49030962da9f49797585eb24c427bffa4fa17/pyometiff-1.0.0.tar.gz", hash = "sha256:596b7a7377a5f2e50292aa52dd22e6347a2d2f21577e93d1fcdcd63942e597f7", size = 37703 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/77/7e8d33d76d0bd6091f88a3c49030962da9f49797585eb24c427bffa4fa17/pyometiff-1.0.0.tar.gz", hash = "sha256:596b7a7377a5f2e50292aa52dd22e6347a2d2f21577e93d1fcdcd63942e597f7", size = 37703, upload-time = "2024-02-03T00:45:09.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/ef/2393ce39fb4f730acc930d0646f9d320ccad10032cb6bd5902f4b2429b11/pyometiff-1.0.0-py3-none-any.whl", hash = "sha256:6c274786f32b9bd662105af37419c7f2f7ee6194bc1a7283b4ab2a00c53cf632", size = 37405 }, + { url = "https://files.pythonhosted.org/packages/56/ef/2393ce39fb4f730acc930d0646f9d320ccad10032cb6bd5902f4b2429b11/pyometiff-1.0.0-py3-none-any.whl", hash = "sha256:6c274786f32b9bd662105af37419c7f2f7ee6194bc1a7283b4ab2a00c53cf632", size = 37405, upload-time = "2024-02-03T00:45:07.616Z" }, ] [[package]] name = "pyparsing" version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/3a/31fd28064d016a2182584d579e033ec95b809d8e220e74c4af6f0f2e8842/pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", size = 889571 } +sdist = { url = "https://files.pythonhosted.org/packages/46/3a/31fd28064d016a2182584d579e033ec95b809d8e220e74c4af6f0f2e8842/pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", size = 889571, upload-time = "2024-03-06T07:25:54.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/ea/6d76df31432a0e6fdf81681a895f009a4bb47b3c39036db3e1b528191d52/pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742", size = 103245 }, + { url = "https://files.pythonhosted.org/packages/9d/ea/6d76df31432a0e6fdf81681a895f009a4bb47b3c39036db3e1b528191d52/pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742", size = 103245, upload-time = "2024-03-06T07:25:50.845Z" }, ] [[package]] @@ -598,9 +745,9 @@ dependencies = [ { name = "packaging" }, { name = "pluggy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314 } +sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314, upload-time = "2024-07-25T10:40:00.159Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802 }, + { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802, upload-time = "2024-07-25T10:39:57.834Z" }, ] [[package]] @@ -611,9 +758,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, ] [[package]] @@ -624,7 +771,7 @@ dependencies = [ { name = "pytest" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/3b/317cc04e77d707d338540ca67b619df8f247f3f4c9f40e67bf5ea503ad94/pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1", size = 19499 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/3b/317cc04e77d707d338540ca67b619df8f247f3f4c9f40e67bf5ea503ad94/pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1", size = 19499, upload-time = "2023-12-31T20:38:54.991Z" } [[package]] name = "python-dateutil" @@ -633,25 +780,25 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867 }, - { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530 }, - { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244 }, - { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871 }, - { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729 }, - { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528 }, - { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286 }, - { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699 }, + { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload-time = "2023-07-17T23:57:34.35Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload-time = "2023-07-17T23:57:36.975Z" }, + { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload-time = "2023-07-17T23:57:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload-time = "2023-07-17T23:57:51.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload-time = "2023-07-17T23:57:59.865Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload-time = "2023-08-28T18:43:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload-time = "2023-07-17T23:58:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, ] [[package]] @@ -661,32 +808,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, ] [[package]] name = "regex" version = "2024.7.24" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/51/64256d0dc72816a4fe3779449627c69ec8fee5a5625fd60ba048f53b3478/regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506", size = 393485 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/51/64256d0dc72816a4fe3779449627c69ec8fee5a5625fd60ba048f53b3478/regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506", size = 393485, upload-time = "2024-07-24T21:51:16.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/ec/261f8434a47685d61e59a4ef3d9ce7902af521219f3ebd2194c7adb171a6/regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281", size = 470810 }, - { url = "https://files.pythonhosted.org/packages/f0/47/f33b1cac88841f95fff862476a9e875d9a10dae6912a675c6f13c128e5d9/regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b", size = 282126 }, - { url = "https://files.pythonhosted.org/packages/fc/1b/256ca4e2d5041c0aa2f1dc222f04412b796346ab9ce2aa5147405a9457b4/regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a", size = 278920 }, - { url = "https://files.pythonhosted.org/packages/91/03/4603ec057c0bafd2f6f50b0bdda4b12a0ff81022decf1de007b485c356a6/regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73", size = 785420 }, - { url = "https://files.pythonhosted.org/packages/75/f8/13b111fab93e6273e26de2926345e5ecf6ddad1e44c4d419d7b0924f9c52/regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2", size = 828164 }, - { url = "https://files.pythonhosted.org/packages/4a/80/bc3b9d31bd47ff578758af929af0ac1d6169b247e26fa6e87764007f3d93/regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e", size = 812621 }, - { url = "https://files.pythonhosted.org/packages/8b/77/92d4a14530900d46dddc57b728eea65d723cc9fcfd07b96c2c141dabba84/regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51", size = 786609 }, - { url = "https://files.pythonhosted.org/packages/35/58/06695fd8afad4c8ed0a53ec5e222156398b9fe5afd58887ab94ea68e4d16/regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364", size = 775290 }, - { url = "https://files.pythonhosted.org/packages/1b/0f/50b97ee1fc6965744b9e943b5c0f3740792ab54792df73d984510964ef29/regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee", size = 772849 }, - { url = "https://files.pythonhosted.org/packages/8f/64/565ff6cf241586ab7ae76bb4138c4d29bc1d1780973b457c2db30b21809a/regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c", size = 778428 }, - { url = "https://files.pythonhosted.org/packages/e5/fe/4ceabf4382e44e1e096ac46fd5e3bca490738b24157116a48270fd542e88/regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce", size = 849436 }, - { url = "https://files.pythonhosted.org/packages/68/23/1868e40d6b594843fd1a3498ffe75d58674edfc90d95e18dd87865b93bf2/regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1", size = 849484 }, - { url = "https://files.pythonhosted.org/packages/f3/52/bff76de2f6e2bc05edce3abeb7e98e6309aa022fc06071100a0216fbeb50/regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e", size = 776712 }, - { url = "https://files.pythonhosted.org/packages/f2/72/70ade7b0b5fe5c6df38fdfa2a5a8273e3ea6a10b772aa671b7e889e78bae/regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c", size = 257716 }, - { url = "https://files.pythonhosted.org/packages/04/4d/80e04f4e27ab0cbc9096e2d10696da6d9c26a39b60db52670fd57614fea5/regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52", size = 269662 }, + { url = "https://files.pythonhosted.org/packages/cb/ec/261f8434a47685d61e59a4ef3d9ce7902af521219f3ebd2194c7adb171a6/regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281", size = 470810, upload-time = "2024-07-24T21:47:22.913Z" }, + { url = "https://files.pythonhosted.org/packages/f0/47/f33b1cac88841f95fff862476a9e875d9a10dae6912a675c6f13c128e5d9/regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b", size = 282126, upload-time = "2024-07-24T21:47:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/256ca4e2d5041c0aa2f1dc222f04412b796346ab9ce2aa5147405a9457b4/regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a", size = 278920, upload-time = "2024-07-24T21:47:28.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/03/4603ec057c0bafd2f6f50b0bdda4b12a0ff81022decf1de007b485c356a6/regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73", size = 785420, upload-time = "2024-07-24T21:47:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/75/f8/13b111fab93e6273e26de2926345e5ecf6ddad1e44c4d419d7b0924f9c52/regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2", size = 828164, upload-time = "2024-07-24T21:47:33.857Z" }, + { url = "https://files.pythonhosted.org/packages/4a/80/bc3b9d31bd47ff578758af929af0ac1d6169b247e26fa6e87764007f3d93/regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e", size = 812621, upload-time = "2024-07-24T21:47:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/8b/77/92d4a14530900d46dddc57b728eea65d723cc9fcfd07b96c2c141dabba84/regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51", size = 786609, upload-time = "2024-07-24T21:47:40.483Z" }, + { url = "https://files.pythonhosted.org/packages/35/58/06695fd8afad4c8ed0a53ec5e222156398b9fe5afd58887ab94ea68e4d16/regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364", size = 775290, upload-time = "2024-07-24T21:47:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0f/50b97ee1fc6965744b9e943b5c0f3740792ab54792df73d984510964ef29/regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee", size = 772849, upload-time = "2024-07-24T21:47:47.04Z" }, + { url = "https://files.pythonhosted.org/packages/8f/64/565ff6cf241586ab7ae76bb4138c4d29bc1d1780973b457c2db30b21809a/regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c", size = 778428, upload-time = "2024-07-24T21:47:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/4ceabf4382e44e1e096ac46fd5e3bca490738b24157116a48270fd542e88/regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce", size = 849436, upload-time = "2024-07-24T21:47:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/68/23/1868e40d6b594843fd1a3498ffe75d58674edfc90d95e18dd87865b93bf2/regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1", size = 849484, upload-time = "2024-07-24T21:47:56.969Z" }, + { url = "https://files.pythonhosted.org/packages/f3/52/bff76de2f6e2bc05edce3abeb7e98e6309aa022fc06071100a0216fbeb50/regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e", size = 776712, upload-time = "2024-07-24T21:47:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/f2/72/70ade7b0b5fe5c6df38fdfa2a5a8273e3ea6a10b772aa671b7e889e78bae/regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c", size = 257716, upload-time = "2024-07-24T21:48:03.094Z" }, + { url = "https://files.pythonhosted.org/packages/04/4d/80e04f4e27ab0cbc9096e2d10696da6d9c26a39b60db52670fd57614fea5/regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52", size = 269662, upload-time = "2024-07-24T21:48:06.441Z" }, ] [[package]] @@ -699,18 +846,18 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "setuptools" version = "72.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/11/487b18cc768e2ae25a919f230417983c8d5afa1b6ee0abd8b6db0b89fa1d/setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec", size = 2419487 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/11/487b18cc768e2ae25a919f230417983c8d5afa1b6ee0abd8b6db0b89fa1d/setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec", size = 2419487, upload-time = "2024-07-29T15:11:43.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/58/e0ef3b9974a04ce9cde2a7a33881ddcb2d68450803745804545cdd8d258f/setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1", size = 2337965 }, + { url = "https://files.pythonhosted.org/packages/e1/58/e0ef3b9974a04ce9cde2a7a33881ddcb2d68450803745804545cdd8d258f/setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1", size = 2337965, upload-time = "2024-07-29T15:11:37.999Z" }, ] [[package]] @@ -718,20 +865,20 @@ name = "simpleitk" version = "2.3.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/3a/bb0f2e759f288c89923f9cf099daf81a1350af3035fd992288943ac49709/SimpleITK-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df43581c6984af5353730834a95116cdd8dcaef1dc13e4e9a326f608f8fba74a", size = 44888897 }, - { url = "https://files.pythonhosted.org/packages/06/09/b7a2ad87128d7da4989467b368c6afb17c6d78448a7da90f6ab27a6e2af9/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd1c3d7f65bf5855013121bd9f2a683f4c429b746f5cc41f84af08dd28c92573", size = 29856558 }, - { url = "https://files.pythonhosted.org/packages/e6/45/41a228e7c54edb52397cf4b0b3922f04dd15d247d32cdce08badd410e520/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d088fbfbbd639aebe99aed0d9cf69364e2e8e7f4771fa2acc1017f1126a497c", size = 47951076 }, - { url = "https://files.pythonhosted.org/packages/60/6c/466f3f44070844b5e83a43310c2ec464c0bcd346244615d8c734c527490e/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cd45cf257e1a4469576e90fd0c476685b3b66d1f471d34677dd7f3876601b6", size = 52655126 }, - { url = "https://files.pythonhosted.org/packages/b3/e6/8510b79a8ab93248fb897c0ca3bc2c65a6cfca478ad7d5a53e950dd9b17b/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:2aec554a4656dc84195711a8ebfe89477aa66dce2d8c2fd81890ea96ecb725fb", size = 18074454 }, + { url = "https://files.pythonhosted.org/packages/af/3a/bb0f2e759f288c89923f9cf099daf81a1350af3035fd992288943ac49709/SimpleITK-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df43581c6984af5353730834a95116cdd8dcaef1dc13e4e9a326f608f8fba74a", size = 44888897, upload-time = "2023-11-06T14:56:03.652Z" }, + { url = "https://files.pythonhosted.org/packages/06/09/b7a2ad87128d7da4989467b368c6afb17c6d78448a7da90f6ab27a6e2af9/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd1c3d7f65bf5855013121bd9f2a683f4c429b746f5cc41f84af08dd28c92573", size = 29856558, upload-time = "2023-11-06T14:58:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/e6/45/41a228e7c54edb52397cf4b0b3922f04dd15d247d32cdce08badd410e520/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d088fbfbbd639aebe99aed0d9cf69364e2e8e7f4771fa2acc1017f1126a497c", size = 47951076, upload-time = "2023-11-06T15:01:25.611Z" }, + { url = "https://files.pythonhosted.org/packages/60/6c/466f3f44070844b5e83a43310c2ec464c0bcd346244615d8c734c527490e/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cd45cf257e1a4469576e90fd0c476685b3b66d1f471d34677dd7f3876601b6", size = 52655126, upload-time = "2023-11-06T14:56:07.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e6/8510b79a8ab93248fb897c0ca3bc2c65a6cfca478ad7d5a53e950dd9b17b/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:2aec554a4656dc84195711a8ebfe89477aa66dce2d8c2fd81890ea96ecb725fb", size = 18074454, upload-time = "2023-11-06T14:56:10.795Z" }, ] [[package]] name = "six" version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041, upload-time = "2021-05-05T14:18:18.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload-time = "2021-05-05T14:18:17.237Z" }, ] [[package]] @@ -741,37 +888,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/a0/8f93f5281f46763b147ce2c7b37fa1cc675fb09c5ef347a8f5a36997c93d/tifffile-2024.7.24.tar.gz", hash = "sha256:723456ebf2b4918878ae05a7b50fa366ff3b3a686293317eb7a0f294c3eea050", size = 362806 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/a0/8f93f5281f46763b147ce2c7b37fa1cc675fb09c5ef347a8f5a36997c93d/tifffile-2024.7.24.tar.gz", hash = "sha256:723456ebf2b4918878ae05a7b50fa366ff3b3a686293317eb7a0f294c3eea050", size = 362806, upload-time = "2024-07-24T04:11:14.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/a9/f7b3fd6c73e0ac29e7f9c9c86c3b7367b182a3dd60495da7b8129e6df681/tifffile-2024.7.24-py3-none-any.whl", hash = "sha256:f5cce1a915c37bc44ae4a792e3b42c07a30a3fa88406f5c6060a3de076487ed1", size = 226196 }, + { url = "https://files.pythonhosted.org/packages/05/a9/f7b3fd6c73e0ac29e7f9c9c86c3b7367b182a3dd60495da7b8129e6df681/tifffile-2024.7.24-py3-none-any.whl", hash = "sha256:f5cce1a915c37bc44ae4a792e3b42c07a30a3fa88406f5c6060a3de076487ed1", size = 226196, upload-time = "2024-07-24T04:11:11.197Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "tomlkit" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/34/f5f4fbc6b329c948a90468dd423aaa3c3bfc1e07d5a76deec269110f2f6e/tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72", size = 191792 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/34/f5f4fbc6b329c948a90468dd423aaa3c3bfc1e07d5a76deec269110f2f6e/tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72", size = 191792, upload-time = "2024-07-10T09:25:56.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7c/b753bf603852cab0a660da6e81f4ea5d2ca0f0b2b4870766d7aa9bceb7a2/tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264", size = 37770 }, + { url = "https://files.pythonhosted.org/packages/fd/7c/b753bf603852cab0a660da6e81f4ea5d2ca0f0b2b4870766d7aa9bceb7a2/tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264", size = 37770, upload-time = "2024-07-10T09:25:54.676Z" }, ] [[package]] @@ -781,57 +928,57 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/1e/626c2d87c29a35fadc8de5624f4302e1ee56cff380d282d62cb3780e6620/transforms3d-0.4.2.tar.gz", hash = "sha256:e8b5df30eaedbee556e81c6938e55aab5365894e47d0a17615d7db7fd2393680", size = 1368797 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/1e/626c2d87c29a35fadc8de5624f4302e1ee56cff380d282d62cb3780e6620/transforms3d-0.4.2.tar.gz", hash = "sha256:e8b5df30eaedbee556e81c6938e55aab5365894e47d0a17615d7db7fd2393680", size = 1368797, upload-time = "2024-06-17T11:43:33.231Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl", hash = "sha256:1c70399d9e9473ecc23311fd947f727f7c69ed0b063244828c383aa1aefa5941", size = 1376759 }, + { url = "https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl", hash = "sha256:1c70399d9e9473ecc23311fd947f727f7c69ed0b063244828c383aa1aefa5941", size = 1376759, upload-time = "2024-06-20T11:09:19.43Z" }, ] [[package]] name = "urllib3" version = "2.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266, upload-time = "2024-06-17T13:40:11.401Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444, upload-time = "2024-06-17T13:40:07.795Z" }, ] [[package]] name = "verspec" version = "0.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640 }, + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] [[package]] name = "watchdog" version = "4.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/f9/b01e4632aed9a6ecc2b3e501feffd3af5aa0eb4e3b0283fc9525bf503c38/watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", size = 126583 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/f9/b01e4632aed9a6ecc2b3e501feffd3af5aa0eb4e3b0283fc9525bf503c38/watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", size = 126583, upload-time = "2024-05-23T16:12:28.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bc/a1ce8b77eede5a2f4fbcdc923079eb85b7c6e0f5e366ad06661b4dd807e1/watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", size = 101627 }, - { url = "https://files.pythonhosted.org/packages/c2/84/9c66fb603bb683fe559ceeba8f3d5dbea3293b631b2eba319d7d47a2d7fb/watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", size = 92464 }, - { url = "https://files.pythonhosted.org/packages/5a/a5/72b9557e77ac3e6c41816fb16f643069b17cf21f745d26e2931cb1bf136c/watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", size = 92953 }, - { url = "https://files.pythonhosted.org/packages/3a/36/28ce38b960f2bf1e1be573d85e8127c9ac66b4de63a7bef3f61b3f77ce57/watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", size = 83011 }, - { url = "https://files.pythonhosted.org/packages/05/7b/efc5b4134c97f08b161faa703327cde3fe647c5c48c156fde0c343471095/watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", size = 83009 }, - { url = "https://files.pythonhosted.org/packages/c3/bb/1fac328ba90ea091ef04e7bdefe638a933076530d802c1b1cf1f03fe7e89/watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", size = 83011 }, - { url = "https://files.pythonhosted.org/packages/ce/df/c8719022af772d9f75f1c49af453a48a785a45295bca1ce4f3f55b9923af/watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", size = 83012 }, - { url = "https://files.pythonhosted.org/packages/b0/d5/7285d52e7a7ffce2ae0b21a98dbbed345bcb227672a4268eb26d046d8d41/watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", size = 83011 }, - { url = "https://files.pythonhosted.org/packages/2a/09/4b07dc8dd1a9f67a7acfbc084f26fc35ee8a2e4feeb0a2c98fe9a1ef196c/watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5", size = 83009 }, - { url = "https://files.pythonhosted.org/packages/24/01/a4034a94a5f1828eb050230e7cf13af3ac23cf763512b6afe008d3def97c/watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", size = 83012 }, - { url = "https://files.pythonhosted.org/packages/8f/5e/c0d7dad506adedd584188578901871fe923abf6c0c5dc9e79d9be5c7c24e/watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", size = 82996 }, - { url = "https://files.pythonhosted.org/packages/85/e0/2a9f43008902427b5f074c497705d6ef8f815c85d4bc25fbf83f720a6159/watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", size = 83002 }, - { url = "https://files.pythonhosted.org/packages/db/54/23e5845ef68e1817b3792b2a11fb2088d7422814d41af8186d9058c4ff07/watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", size = 83002 }, + { url = "https://files.pythonhosted.org/packages/1c/bc/a1ce8b77eede5a2f4fbcdc923079eb85b7c6e0f5e366ad06661b4dd807e1/watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", size = 101627, upload-time = "2024-05-23T16:11:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/c2/84/9c66fb603bb683fe559ceeba8f3d5dbea3293b631b2eba319d7d47a2d7fb/watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", size = 92464, upload-time = "2024-05-23T16:11:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a5/72b9557e77ac3e6c41816fb16f643069b17cf21f745d26e2931cb1bf136c/watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", size = 92953, upload-time = "2024-05-23T16:11:40.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/36/28ce38b960f2bf1e1be573d85e8127c9ac66b4de63a7bef3f61b3f77ce57/watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", size = 83011, upload-time = "2024-05-23T16:12:10.641Z" }, + { url = "https://files.pythonhosted.org/packages/05/7b/efc5b4134c97f08b161faa703327cde3fe647c5c48c156fde0c343471095/watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", size = 83009, upload-time = "2024-05-23T16:12:12.692Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/1fac328ba90ea091ef04e7bdefe638a933076530d802c1b1cf1f03fe7e89/watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", size = 83011, upload-time = "2024-05-23T16:12:14.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/df/c8719022af772d9f75f1c49af453a48a785a45295bca1ce4f3f55b9923af/watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", size = 83012, upload-time = "2024-05-23T16:12:15.893Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/7285d52e7a7ffce2ae0b21a98dbbed345bcb227672a4268eb26d046d8d41/watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", size = 83011, upload-time = "2024-05-23T16:12:17.545Z" }, + { url = "https://files.pythonhosted.org/packages/2a/09/4b07dc8dd1a9f67a7acfbc084f26fc35ee8a2e4feeb0a2c98fe9a1ef196c/watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5", size = 83009, upload-time = "2024-05-23T16:12:19.496Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/a4034a94a5f1828eb050230e7cf13af3ac23cf763512b6afe008d3def97c/watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", size = 83012, upload-time = "2024-05-23T16:12:21.754Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5e/c0d7dad506adedd584188578901871fe923abf6c0c5dc9e79d9be5c7c24e/watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", size = 82996, upload-time = "2024-05-23T16:12:23.167Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/2a9f43008902427b5f074c497705d6ef8f815c85d4bc25fbf83f720a6159/watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", size = 83002, upload-time = "2024-05-23T16:12:24.981Z" }, + { url = "https://files.pythonhosted.org/packages/db/54/23e5845ef68e1817b3792b2a11fb2088d7422814d41af8186d9058c4ff07/watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", size = 83002, upload-time = "2024-05-23T16:12:27.101Z" }, ] [[package]] name = "zipp" version = "3.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/20/b48f58857d98dcb78f9e30ed2cfe533025e2e9827bbd36ea0a64cc00cbc1/zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", size = 22922 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/20/b48f58857d98dcb78f9e30ed2cfe533025e2e9827bbd36ea0a64cc00cbc1/zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", size = 22922, upload-time = "2024-06-04T17:21:09.042Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/38/f5c473fe9b90c8debdd29ea68d5add0289f1936d6f923b6b9cc0b931194c/zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c", size = 9039 }, + { url = "https://files.pythonhosted.org/packages/20/38/f5c473fe9b90c8debdd29ea68d5add0289f1936d6f923b6b9cc0b931194c/zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c", size = 9039, upload-time = "2024-06-04T17:21:07.146Z" }, ] [[package]] @@ -841,22 +988,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699 }, - { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681 }, - { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328 }, - { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955 }, - { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944 }, - { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927 }, - { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910 }, - { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544 }, - { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094 }, - { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440 }, - { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091 }, - { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682 }, - { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707 }, - { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792 }, - { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586 }, - { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420 }, + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, ]