From e6ada5028d88c47714ea35d9a0a08d621b119ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Thu, 26 Sep 2024 13:33:25 +0200 Subject: [PATCH] refactor: Generate extension semantically --- oscillode/__init__.py | 14 -- oscillode/blender_manifest.toml | 62 --------- pyproject-rye.toml | 180 ------------------------ pyproject.toml | 65 ++++++++- scripts/bl_delete_addon.py | 109 --------------- scripts/bl_init.py | 67 +++++++++ scripts/bl_install_addon.py | 122 ---------------- scripts/dev.py | 120 ---------------- scripts/get-wheels.py | 46 ------ scripts/info.py | 82 +++++------ scripts/pack.py | 238 +++++++++++++++++++++----------- uv.lock | 42 ++++-- 12 files changed, 350 insertions(+), 797 deletions(-) delete mode 100644 oscillode/blender_manifest.toml delete mode 100644 pyproject-rye.toml delete mode 100644 scripts/bl_delete_addon.py create mode 100644 scripts/bl_init.py delete mode 100644 scripts/bl_install_addon.py delete mode 100644 scripts/dev.py delete mode 100644 scripts/get-wheels.py diff --git a/oscillode/__init__.py b/oscillode/__init__.py index 6bf2928..597c57b 100644 --- a/oscillode/__init__.py +++ b/oscillode/__init__.py @@ -83,20 +83,6 @@ from .nodeps.utils import pydeps # noqa: E402 log = simple_logger.get(__name__) -#################### -# - Addon Information -#################### -bl_info = { - 'name': 'Maxwell PDE Sim and Viz', - 'blender': (4, 1, 0), - 'category': 'Node', - 'description': 'Placeholder', - 'author': 'Sofus Albert Høgsbro Rose', - 'version': (0, 0, 0), - 'wiki_url': 'https://git.sofus.io/dtu-courses/bsc_thesis', - 'tracker_url': 'https://git.sofus.io/dtu-courses/bsc_thesis/issues', -} - #################### # - Load and Register Addon diff --git a/oscillode/blender_manifest.toml b/oscillode/blender_manifest.toml deleted file mode 100644 index 400fb99..0000000 --- a/oscillode/blender_manifest.toml +++ /dev/null @@ -1,62 +0,0 @@ -# https://docs.blender.org/manual/en/4.2/extensions/getting_started.html - -schema_version = "1.0.0" - -# Basics -id = "oscillode" -version = "0.2.0" -name = "Oscillode" -tagline = "Nodes for oscillating Maxwell sim and analysis" -maintainer = "Sofus Albert Høgsbro Rose " - -# Blender Compatibility -type = "add-on" -blender_version_min = "4.2.0" -blender_version_max = "4.3.0" - -# OS/Arch Compatibility -platforms = ["linux-x86_64"] -## wheels = ?? - -# Permissions -# * "files" (for access of any filesystem operations) -# * "network" (for internet access) -# * "clipboard" (to read and/or write the system clipboard) -# * "camera" (to capture photos and videos) -# * "microphone" (to capture audio) -permissions = ["files", "network"] - -# Addon Tags -## https://docs.blender.org/manual/en/dev/extensions/tags.html -tags = ["Node", "Scene", "Import-Export"] - -# License / Copyright (use "SPDX: prefix) -## https://spdx.org/licenses/ -license = [ - "SPDX:AGPL-3.0-or-later", -] -copyright = [ - "2024 Oscillode Contributors", -] - -# Support -website = "https://oscillode.io" - -# Optional list of supported platforms. If omitted, the extension will be available in all operating systems. -# platforms = ["windows-amd64", "macos-arm64", "linux-x86_64"] -# Other supported platforms: "windows-arm64", "macos-x86_64" - -# Optional: bundle 3rd party Python modules. -# https://docs.blender.org/manual/en/dev/extensions/python_wheels.html -# wheels = [ -# "./wheels/hexdump-3.3-py3-none-any.whl", -# "./wheels/jsmin-3.0.1-py3-none-any.whl" -# ] - -# Optional: build setting. -# https://docs.blender.org/manual/en/dev/extensions/command_line_arguments.html#command-line-args-extension-build -# [build] -# paths_exclude_pattern = [ -# "/.git/" -# "__pycache__/" -# ] diff --git a/pyproject-rye.toml b/pyproject-rye.toml deleted file mode 100644 index 019731e..0000000 --- a/pyproject-rye.toml +++ /dev/null @@ -1,180 +0,0 @@ -[project] -name = "blender_maxwell" -version = "0.1.0" -description = "Real-time design and visualization of Maxwell simulations in Blender 3D, with deep Tidy3D integration. " -authors = [ - { name = "Sofus Albert Høgsbro Rose", email = "blender-maxwell@sofusrose.com" } -] -dependencies = [ - "tidy3d==2.7.0rc2", - "pydantic>=2.7.1", - "sympy==1.12", - "scipy==1.12.*", - "trimesh==4.2.*", - "networkx==3.2.*", - "rich>=13.7.1", - "rtree==1.2.*", - "jax[cpu]==0.4.26", - "msgspec[toml]==0.18.6", - "numba==0.59.1", - "jaxtyping==0.2.28", - "polars>=0.20.26", - "seaborn[stats]>=0.13.2", - "frozendict>=2.4.4", - "pydantic-tensor>=0.2.0", - # Pin Blender 4.1.0-Compatible Versions - ## The dependency resolver will report if anything is wonky. - "urllib3==1.26.8", - #"requests==2.27.1", ## Conflict with dev-dep commitizen - "numpy==1.24.3", - "idna==3.3", - #"charset-normalizer==2.0.10", ## Conflict with dev-dep commitizen - "certifi==2021.10.8", - "pip>=24.2", -] -## When it comes to dev-dep conflicts: -## -> It's okay to leave Blender-pinned deps out of prod; Blender still has them. -## -> In edge cases, other deps might grab newer versions and Blender will complain. -## -> Let's wait and see if this is more than a theoretical issue. -readme = "README.md" -requires-python = "~= 3.11" -license = { text = "AGPL-3.0-or-later" } - -#################### -# - Tooling: Rye -#################### -[tool.rye] -managed = true -virtual = true -dev-dependencies = [ - "ruff>=0.4.3", - "fake-bpy-module-4-0>=20231118", - "pre-commit>=3.7.0", - "commitizen>=3.25.0", - ## Requires charset-normalizer>=2.1.0 - # Required by Commitizen - ## -> It's okay to have different dev/prod versions in our use case. - "charset-normalizer==2.1.*", - ## Manually scanned CHANGELOG; seems compatible. -] - -[tool.rye.scripts] -dev = "python ./src/scripts/dev.py" -pack = "python ./src/scripts/pack.py" - - -#################### -# - Tooling: Ruff -#################### -[tool.ruff] -target-version = "py311" -line-length = 88 -pycodestyle.max-doc-length = 120 - -[tool.ruff.lint] -task-tags = ["TODO"] -select = [ - "E", # pycodestyle ## General Purpose - "F", # pyflakes ## General Purpose - "PL", # Pylint ## General Purpose - - ## Code Quality - "TCH", # flake8-type-checking ## Type Checking Block Validator - "C90", # mccabe ## Avoid Too-Complex Functions - "ERA", # eradicate ## Ban Commented Code - "TRY", # tryceratops ## Exception Handling Style - "B", # flake8-bugbear ## Opinionated, Probable-Bug Patterns - "N", # pep8-naming - "D", # pydocstyle - "SIM", # flake8-simplify ## Sanity-Check for Code Simplification - "SLF", # flake8-self ## Ban Private Member Access - "RUF", # Ruff-specific rules ## Extra Good-To-Have Rules - - ## Style - "I", # isort ## Force import Sorting - "UP", # pyupgrade ## Enforce Upgrade to Newer Python Syntaxes - "COM", # flake8-commas ## Enforce Trailing Commas - "Q", # flake8-quotes ## Finally - Quoting Style! - "PTH", # flake8-use-pathlib ## Enforce pathlib usage - "A", # flake8-builtins ## Prevent Builtin Shadowing - "C4", # flake9-comprehensions ## Check Compehension Appropriateness - "DTZ", # flake8-datetimez ## Ban naive Datetime Creation - "EM", # flake8-errmsg ## Check Exception String Formatting - "ISC", # flake8-implicit-str-concat ## Enforce Good String Literal Concat - "G", # flake8-logging-format ## Enforce Good Logging Practices - "INP", # flake8-no-pep420 ## Ban PEP420; Enforce __init__.py. - "PIE", # flake8-pie ## Misc Opinionated Checks - "T20", # flake8-print ## Ban print() - "RSE", # flake8-raise ## Check Niche Exception Raising Pattern - "RET", # flake8-return ## Enforce Good Returning - "ARG", # flake8-unused-arguments ## Ban Unused Arguments - - # Specific - "PT", # flake8-pytest-style ## pytest-Specific Checks -] -ignore = [ - "COM812", # Conflicts w/Formatter - "ISC001", # Conflicts w/Formatter - "Q000", # Conflicts w/Formatter - "Q001", # Conflicts w/Formatter - "Q002", # Conflicts w/Formatter - "Q003", # Conflicts w/Formatter - "D206", # Conflicts w/Formatter - "B008", # FastAPI uses this for Depends(), Security(), etc. . - "E701", # class foo(Parent): pass or if simple: return are perfectly elegant - "ERA001", # 'Commented-out code' seems to be just about anything to ruff - "F722", # jaxtyping uses type annotations that ruff sees as "syntax error" - "N806", # Sometimes we like using types w/uppercase in functions, sue me - "RUF001", # We use a lot of unicode, yes, on purpose! - - # Line Length - Controversy Incoming - ## Hot Take: Let the Formatter Worry about Line Length - ## - Yes dear reader, I'm with you. Soft wrap can go too far. - ## - ...but also, sometimes there are real good reasons not to split. - ## - Ex. I think 'one sentence per line' docstrings are a valid thing. - ## - Overlong lines tend to be be a code smell anyway - ## - We'll see if my hot takes survive the week :) - "E501", # Let Formatter Worry about Line Length -] - -#################### -# - Tooling: Ruff Sublinters -#################### -[tool.ruff.lint.flake8-bugbear] -extend-immutable-calls = [] - -[tool.ruff.lint.pycodestyle] -ignore-overlong-task-comments = true - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.pylint] -max-args = 6 - -#################### -# - Tooling: Ruff Formatter -#################### -[tool.ruff.format] -quote-style = "single" -indent-style = "tab" -docstring-code-format = false - -#################### -# - Tooling: Commits -#################### -[tool.commitizen] -# Specification -name = "cz_conventional_commits" -version_scheme = "semver2" -version_provider = "pep621" -tag_format = "v$version" - -# Version Bumping -retry_after_failure = true -major_version_zero = true -update_changelog_on_bump = true - -# Annotations / Signature -gpg_sign = true -annotated_tag = true diff --git a/pyproject.toml b/pyproject.toml index 038f34a..548a34a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,14 @@ description = "Real-time design and visualization of Maxwell simulations in Blen authors = [ { name = "Sofus Albert Høgsbro Rose", email = "oscillode@sofusrose.com" } ] +maintainers = [ + { name = "Sofus Albert Høgsbro Rose", email = "oscillode@sofusrose.com" } +] dependencies = [ "tidy3d==2.7.*", "pydantic==2.9.*", "sympy==1.13.*", - "scipy==1.13.*", + "scipy==1.14.*", "trimesh==4.4.*", "networkx==3.3.*", "rich>=13.8.*", @@ -28,6 +31,64 @@ readme = "README.md" requires-python = "~= 3.11" license = { text = "AGPL-3.0-or-later" } +[project.urls] +Homepage = "https://oscillode.io" + +#################### +# - Blender Extension +#################### +[tool.bl_ext] +pretty_name = "Oscillode" +blender_version_min = '4.2.0' +blender_version_max = '4.2.2' +permissions = ["files", "network"] +bl_tags = ["Node", "Scene", "Import-Export"] +copyright = ["2024 Oscillode Contributors"] + +# Platform Support +## Map Valid Blender Platforms -> Required PyPi Platform Tags +## Include as few PyPi tags as works on ~everything. +[tool.bl_ext.platforms] +#windows-amd64 = ['win_amd64'] +#macos-arm64 = ['macosx_11_0_arm64', 'macosx_12_0_arm64', 'macosx_14_0_arm64'] +linux-x86_64 = ['manylinux1_x86_64', 'manylinux2014_x86_64', 'manylinux_2_17_x86_64'] + +#macos-x86_64 = ['macosx_10_10_x86_64'] ##TODO: Broken + +# Packaging +## Path is from the directory containing this file. +[tool.bl_ext.packaging] +path_builds = 'dev/build' +path_wheels = 'dev/wheels' +path_local = 'dev/local' +init_settings_filename = 'init_settings.toml' + +# "Profiles" -> Affects Initialization Settings +## This sets the default extension preferences for different situations. +[tool.bl_ext.profiles.dev] +use_path_local = true +use_log_file = true +log_file_path = 'oscillode.log' +log_file_level = 'debug' +use_log_console = true +log_console_level = 'info' + +[tool.bl_ext.profiles.release] +use_path_local = false +use_log_file = true +log_file_path = 'oscillode.log' +log_file_level = 'info' +use_log_console = true +log_console_level = 'warning' + +[tool.bl_ext.profiles.release-debug] +use_path_local = false +use_log_file = true +log_file_path = 'oscillode.log' +log_file_level = 'debug' +use_log_console = true +log_console_level = 'warning' + #################### # - Tooling: Rye #################### @@ -53,7 +114,6 @@ package = false [tool.ruff] target-version = "py311" line-length = 88 -pycodestyle.max-doc-length = 120 [tool.ruff.lint] task-tags = ["TODO"] @@ -128,6 +188,7 @@ ignore = [ extend-immutable-calls = [] [tool.ruff.lint.pycodestyle] +max-doc-length = 120 ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] diff --git a/scripts/bl_delete_addon.py b/scripts/bl_delete_addon.py deleted file mode 100644 index 992c397..0000000 --- a/scripts/bl_delete_addon.py +++ /dev/null @@ -1,109 +0,0 @@ -# oscillode -# Copyright (C) 2024 oscillode Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# blender_maxwell -# Copyright (C) 2024 blender_maxwell Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import logging -import shutil -import sys -import traceback -from pathlib import Path - -import bpy - -PATH_SCRIPT = str(Path(__file__).resolve().parent) -sys.path.insert(0, str(PATH_SCRIPT)) -import info # noqa: E402 - -sys.path.remove(str(PATH_SCRIPT)) - -# Set Bootstrap Log Level -## This will be the log-level of both console and file logs, at first... -## ...until the addon preferences have been loaded. -BOOTSTRAP_LOG_LEVEL = logging.DEBUG - - -def delete_addon_if_loaded(addon_name: str) -> bool: - """Strongly inspired by Blender's addon_utils.py.""" - removed_addon = False - - # Check if Python Module is Loaded - mod = sys.modules.get(addon_name) - # if (mod := sys.modules.get(addon_name)) is None: - # ## It could still be loaded-by-default; then, it's in the prefs list - # is_loaded_now = False - # loads_by_default = addon_name in bpy.context.preferences.addons - # else: - # ## BL sets __addon_enabled__ on module of enabled addons. - # ## BL sets __addon_persistent__ on module of load-by-default addons. - # is_loaded_now = getattr(mod, '__addon_enabled__', False) - # loads_by_default = getattr(mod, '__addon_persistent__', False) - - # Unregister Modules and Mark Disabled & Non-Persistent - ## This effectively disables it - if mod is not None: - removed_addon = True - mod.__addon_enabled__ = False - mod.__addon_persistent__ = False - try: - mod.unregister() - except BaseException: - traceback.print_exc() - - # Remove Addon - ## Remove Addon from Preferences - ## - Unsure why addon_utils has a while, but let's trust the process... - while addon_name in bpy.context.preferences.addons: - addon = bpy.context.preferences.addons.get(addon_name) - if addon: - bpy.context.preferences.addons.remove(addon) - - ## Physically Excise Addon Code - for addons_path in bpy.utils.script_paths(subdir='addons'): - addon_path = Path(addons_path) / addon_name - if addon_path.is_dir(): - shutil.rmtree(addon_path) - - ## Save User Preferences - bpy.ops.wm.save_userpref() - - return removed_addon - - -#################### -# - Main -#################### -if __name__ == '__main__': - if delete_addon_if_loaded(info.ADDON_NAME): - bpy.ops.wm.quit_blender() - sys.exit(info.STATUS_UNINSTALLED_ADDON) - else: - bpy.ops.wm.quit_blender() - sys.exit(info.STATUS_NOCHANGE_ADDON) diff --git a/scripts/bl_init.py b/scripts/bl_init.py new file mode 100644 index 0000000..1e9158d --- /dev/null +++ b/scripts/bl_init.py @@ -0,0 +1,67 @@ +# oscillode +# Copyright (C) 2024 oscillode Project Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# blender_maxwell +# Copyright (C) 2024 blender_maxwell Project Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys +from pathlib import Path + +import bpy + +PATH_SCRIPT = str(Path(__file__).resolve().parent) +sys.path.insert(0, str(PATH_SCRIPT)) +import info # noqa: E402 +import pack # noqa: E402 + +sys.path.remove(str(PATH_SCRIPT)) + + +#################### +# - Main +#################### +if __name__ == '__main__': + pass + # Uninstall Old Extension + # if any(addon_name.endwith(info.ADDON_NAME) and addon_name.startswith('bl_ext.') for addon_name in bpy.context.preferences.addons.keys()): + # bpy.ops.extensions.package_uninstall(pkg_id=info.ADDON_NAME) + + # Install New Extension + # with pack.zipped_addon( + # info.PATH_ADDON_PKG, + # info.PATH_ADDON_ZIP, + # info.PATH_ROOT / 'pyproject.toml', + # info.PATH_ROOT / 'requirements.lock', + # initial_log_level=info.BOOTSTRAP_LOG_LEVEL, + # ) as path_zipped: +# bpy.ops.extensions.package_install_files( +# filepath=path_zipped, +# enable_on_install=True, +# +# ) diff --git a/scripts/bl_install_addon.py b/scripts/bl_install_addon.py deleted file mode 100644 index b4a8d17..0000000 --- a/scripts/bl_install_addon.py +++ /dev/null @@ -1,122 +0,0 @@ -# oscillode -# Copyright (C) 2024 oscillode Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# blender_maxwell -# Copyright (C) 2024 blender_maxwell Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import sys -from pathlib import Path - -import bpy - -PATH_SCRIPT = str(Path(__file__).resolve().parent) -sys.path.insert(0, str(PATH_SCRIPT)) -import info # noqa: E402 -import pack # noqa: E402 - -sys.path.remove(str(PATH_SCRIPT)) - - -def install_and_enable_addon(addon_name: str, addon_zip: Path) -> None: - """Strongly inspired by Blender's addon_utils.py.""" - # Check if Addon is Installable - if any( - [ - (mod := sys.modules.get(addon_name)) is not None, - addon_name in bpy.context.preferences.addons, - any( - (Path(addon_path) / addon_name).exists() - for addon_path in bpy.utils.script_paths(subdir='addons') - ), - ] - ): - in_pref_addons = addon_name in bpy.context.preferences.addons - existing_files_found = { - addon_path: (Path(addon_path) / addon_name).exists() - for addon_path in bpy.utils.script_paths(subdir='addons') - if (Path(addon_path) / addon_name).exists() - } - msg = f"Addon (module = '{mod}') is not installable (in preferences.addons: {in_pref_addons}) (existing files found: {existing_files_found})" - raise ValueError(msg) - - # Install Addon - bpy.ops.preferences.addon_install(filepath=str(addon_zip)) - if not any( - (Path(addon_path) / addon_name).exists() - for addon_path in bpy.utils.script_paths(subdir='addons') - ): - msg = f"Couldn't install addon {addon_name}" - raise RuntimeError(msg) - - # Enable Addon - bpy.ops.preferences.addon_enable(module=addon_name) - if addon_name not in bpy.context.preferences.addons: - msg = f"Couldn't enable addon {addon_name}" - raise RuntimeError(msg) - - # Save User Preferences - bpy.ops.wm.save_userpref() - - -def setup_for_development( - addon_name: str, - path_addon_dev_deps: Path, - path_addon_cache_path: Path | None = None, -) -> None: - addon_prefs = bpy.context.preferences.addons[addon_name].preferences - - # PyDeps Path - addon_prefs.use_default_pydeps_path = False - addon_prefs.pydeps_path = path_addon_dev_deps - if path_addon_cache_path is not None: - addon_prefs.addon_cache_path = path_addon_cache_path - - # Save User Preferences - bpy.ops.wm.save_userpref() - - -#################### -# - Main -#################### -if __name__ == '__main__': - with pack.zipped_addon( - info.PATH_ADDON_PKG, - info.PATH_ADDON_ZIP, - info.PATH_ROOT / 'pyproject.toml', - info.PATH_ROOT / 'requirements.lock', - initial_log_level=info.BOOTSTRAP_LOG_LEVEL, - ) as path_zipped: - install_and_enable_addon(info.ADDON_NAME, path_zipped) - - setup_for_development( - info.ADDON_NAME, info.PATH_ADDON_DEV_DEPS, info.PATH_ADDON_DEV_CACHE - ) - - bpy.ops.wm.quit_blender() - sys.exit(info.STATUS_INSTALLED_ADDON) diff --git a/scripts/dev.py b/scripts/dev.py deleted file mode 100644 index 1befbd7..0000000 --- a/scripts/dev.py +++ /dev/null @@ -1,120 +0,0 @@ -# oscillode -# Copyright (C) 2024 oscillode Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# blender_maxwell -# Copyright (C) 2024 blender_maxwell Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# noqa: INP001 -import os -import subprocess -import sys -from pathlib import Path - -import info - - -#################### -# - Blender Runner -#################### -def run_blender( - py_script: Path | None, - load_devfile: bool = False, - headless: bool = True, - monitor: bool = False, -): - process = subprocess.Popen( - [ - 'blender', - *(['--background'] if headless else []), - *( - [ - '--python', - str(py_script), - ] - if py_script is not None - else [] - ), - *([info.PATH_ADDON_DEV_BLEND] if load_devfile else []), - ], - env=os.environ | {'PYTHONUNBUFFERED': '1'}, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - output = [] - printing_live = monitor - - # Process Real-Time Output - for line in iter(process.stdout.readline, b''): - if not line: - break - - if printing_live: - print(line, end='') # noqa: T201 - elif ( - info.SIGNAL_START_CLEAN_BLENDER in line - # or 'Traceback (most recent call last)' in line - ): - printing_live = True - print(''.join(output)) # noqa: T201 - else: - output.append(line) - - # Wait for the process to finish and get the exit code - process.wait() - return process.returncode, output - - -#################### -# - Main -#################### -if __name__ == '__main__': - # Uninstall Addon - print(f'Blender: Uninstalling "{info.ADDON_NAME}"...') - return_code, output = run_blender(info.PATH_BL_DELETE_ADDON, monitor=False) - if return_code == info.STATUS_UNINSTALLED_ADDON: - print(f'\tBlender: Uninstalled "{info.ADDON_NAME}"') - elif return_code == info.STATUS_NOCHANGE_ADDON: - print(f'\tBlender: "{info.ADDON_NAME}" Not Installed') - - # Install Addon - print(f'Blender: Installing & Enabling "{info.ADDON_NAME}"...') - return_code, output = run_blender(info.PATH_BL_INSTALL_ADDON, monitor=False) - if return_code == info.STATUS_INSTALLED_ADDON: - print(f'\tBlender: Install & Enable "{info.ADDON_NAME}"') - else: - print(f'\tBlender: "{info.ADDON_NAME}" Not Installed') - print(*output, sep='') - sys.exit(1) - - # Run Addon - print(f'Blender: Running "{info.ADDON_NAME}"...') - return_code, output = run_blender( - None, headless=False, load_devfile=True, monitor=True - ) diff --git a/scripts/get-wheels.py b/scripts/get-wheels.py deleted file mode 100644 index ea005a8..0000000 --- a/scripts/get-wheels.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/python - -# oscillode -# Copyright (C) 2024 oscillode Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import sys -import subprocess - -import info - -WHEEL_DOWNLOADS_PATH = info.PATH_BUILD / 'downloads' - -if __name__ == '__main__': - WHEEL_DOWNLOADS_PATH.mkdir(exist_ok=True) - subprocess.check_call( - [ - sys.executable, - '-m', - 'pip', - 'download', - '--requirement', - str(info.PATH_BUILD / 'requirements.txt'), - '--dest', - str(WHEEL_DOWNLOADS_PATH), - '--require-hashes', - '--only-binary', - ':all:', - '--python-version', - '3.11', - '--platform', - 'win_amd64', - ] - ) diff --git a/scripts/info.py b/scripts/info.py index 5e0a336..1d06b0b 100644 --- a/scripts/info.py +++ b/scripts/info.py @@ -30,68 +30,52 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import logging import tomllib from pathlib import Path -PATH_ROOT = Path(__file__).resolve().parent.parent.parent -PATH_SRC = PATH_ROOT / 'src' - -# Scripts -PATH_BL_DELETE_ADDON = PATH_SRC / 'scripts' / 'bl_delete_addon.py' -PATH_BL_INSTALL_ADDON = PATH_SRC / 'scripts' / 'bl_install_addon.py' -PATH_BL_RUN_DEV = PATH_SRC / 'scripts' / 'bl_run_dev.py' - -# Build Dir -PATH_BUILD = PATH_ROOT / 'build' -PATH_BUILD.mkdir(exist_ok=True) - -# Dev Dir -PATH_DEV = PATH_ROOT / 'dev' -PATH_DEV.mkdir(exist_ok=True) - #################### -# - BL_RUN stdout Signals -#################### -SIGNAL_START_CLEAN_BLENDER = 'SIGNAL__blender_is_clean' - -#################### -# - BL_RUN Exit Codes -#################### -STATUS_NOCHANGE_ADDON = 42 -STATUS_UNINSTALLED_ADDON = 42 -STATUS_INSTALLED_ADDON = 69 -STATUS_NOINSTALL_ADDON = 68 - -#################### -# - Addon Information +# - Basic #################### +PATH_ROOT = Path(__file__).resolve().parent.parent with (PATH_ROOT / 'pyproject.toml').open('rb') as f: PROJ_SPEC = tomllib.load(f) -ADDON_NAME = PROJ_SPEC['project']['name'] -ADDON_VERSION = PROJ_SPEC['project']['version'] +REQ_PYTHON_VERSION = PROJ_SPEC['project']['requires-python'].replace('~= ', '') + #################### -# - Packaging Information +# - Paths #################### -PATH_ADDON_PKG = PATH_ROOT / 'src' / ADDON_NAME -PATH_ADDON_ZIP = PATH_ROOT / 'build' / (ADDON_NAME + '__' + ADDON_VERSION + '.zip') +def proc_path(p_str: str) -> Path: + return PATH_ROOT / Path(*p_str.split('/')) -PATH_ADDON_BLEND_STARTER = PATH_ADDON_PKG / 'blenders' / 'starter.blend' -# Set Bootstrap Log Level -## This will be the log-level of both console and file logs, at first... -## ...until the addon preferences have been loaded. -BOOTSTRAP_LOG_LEVEL = logging.DEBUG -BOOTSTRAP_LOG_LEVEL_FILENAME = '.bootstrap_log_level' +PATH_PKG = PATH_ROOT / PROJ_SPEC['project']['name'] + +PATH_DEV = PATH_ROOT / 'dev' +PATH_DEV.mkdir(exist_ok=True) + +# Retrieve Build Path +## Extension ZIP files will be placed here after packaging. +PATH_BUILD = proc_path(PROJ_SPEC['tool']['bl_ext']['packaging']['path_builds']) +PATH_BUILD.mkdir(exist_ok=True) + +# Retrieve Wheels Path +## Dependency wheels will be downloaded to here, then copied into the extension packages. +PATH_WHEELS = proc_path(PROJ_SPEC['tool']['bl_ext']['packaging']['path_wheels']) +PATH_WHEELS.mkdir(exist_ok=True) + +# Retrieve Local Cache Path +## During development, files such as logs will be placed here instead of in the extension's user path. +## This is essentially a debugging convenience. +PATH_LOCAL = proc_path(PROJ_SPEC['tool']['bl_ext']['packaging']['path_local']) +PATH_LOCAL.mkdir(exist_ok=True) -# Install the ZIPped Addon #################### -# - Development Information +# - Computed Paths #################### -PATH_ADDON_DEV_BLEND = PATH_DEV / 'demo.blend' - -PATH_ADDON_DEV_DEPS = PATH_DEV / '.cached-dev-dependencies' -PATH_ADDON_DEV_CACHE = PATH_DEV / '.dev-addon-cache' -PATH_ADDON_DEV_DEPS.mkdir(exist_ok=True) +# Compute Current ZIP to Build +## The concrete path to the file that will be packed and installed. +PATH_ZIP = PATH_BUILD / ( + PROJ_SPEC['project']['name'] + '__' + PROJ_SPEC['project']['version'] + '.zip' +) diff --git a/scripts/pack.py b/scripts/pack.py index 377ab9b..5d2a11d 100644 --- a/scripts/pack.py +++ b/scripts/pack.py @@ -30,37 +30,149 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import sys import contextlib import logging import tempfile import typing as typ import zipfile from pathlib import Path +import itertools +import subprocess + +import tomli_w import info LogLevel: typ.TypeAlias = int -_PROJ_VERSION_STR = str( - tuple(int(el) for el in info.PROJ_SPEC['project']['version'].split('.')) -) -_PROJ_DESC_STR = info.PROJ_SPEC['project']['description'] +BL_EXT__MANIFEST_FILENAME = 'blender_manifest.toml' +BL_EXT__SCHEMA_VERSION = '1.0.0' +BL_EXT__TYPE = 'add-on' -BL_INFO_REPLACEMENTS = { - "'version': (0, 0, 0),": f"'version': {_PROJ_VERSION_STR},", - "'description': 'Placeholder',": f"'description': '{_PROJ_DESC_STR}',", +#################### +# - Generate Manifest +#################### +# See https://docs.blender.org/manual/en/4.2/extensions/getting_started.html +# See https://packaging.python.org/en/latest/guides/writing-pyproject-toml +## TODO: More validation and such. +_FIRST_MAINTAINER = info.PROJ_SPEC['project']['maintainers'][0] +_SPDX_LICENSE_NAME = info.PROJ_SPEC['project']['license']['text'] + +BL_EXT_MANIFEST = { + 'schema_version': BL_EXT__SCHEMA_VERSION, + # Basics + 'id': info.PROJ_SPEC['project']['name'], + 'name': info.PROJ_SPEC['tool']['bl_ext']['pretty_name'], + 'version': info.PROJ_SPEC['project']['version'], + 'tagline': info.PROJ_SPEC['project']['description'], + 'maintainer': f'{_FIRST_MAINTAINER["name"]} <{_FIRST_MAINTAINER["email"]}>', + # Blender Compatibility + 'type': BL_EXT__TYPE, + 'blender_version_min': info.PROJ_SPEC['tool']['bl_ext']['blender_version_min'], + 'blender_version_max': info.PROJ_SPEC['tool']['bl_ext']['blender_version_max'], + 'platforms': list(info.PROJ_SPEC['tool']['bl_ext']['platforms'].keys()), + # OS/Arch Compatibility + ## See https://docs.blender.org/manual/en/dev/extensions/python_wheels.html + 'wheels': [ + f'./wheels/{wheel_path.name}' for wheel_path in info.PATH_WHEELS.iterdir() + ], + # Permissions + ## * "files" (for access of any filesystem operations) + ## * "network" (for internet access) + ## * "clipboard" (to read and/or write the system clipboard) + ## * "camera" (to capture photos and videos) + ## * "microphone" (to capture audio) + 'permissions': info.PROJ_SPEC['tool']['bl_ext']['permissions'], + # Addon Tags + 'tags': info.PROJ_SPEC['tool']['bl_ext']['bl_tags'], + 'license': [f'SPDX:{_SPDX_LICENSE_NAME}'], + 'copyright': info.PROJ_SPEC['tool']['bl_ext']['copyright'], + 'website': info.PROJ_SPEC['project']['urls']['Homepage'], } -@contextlib.contextmanager -def zipped_addon( # noqa: PLR0913 - path_addon_pkg: Path, - path_addon_zip: Path, - path_pyproject_toml: Path, - path_requirements_lock: Path, - initial_log_level: LogLevel = logging.INFO, +#################### +# - Generate Init Settings +#################### +def generate_init_settings_dict(profile: str) -> dict: + profile_settings = info.PROJ_SPEC['tool']['bl_ext']['profiles'][profile] + + if profile_settings['use_path_local']: + base_path = info.PATH_LOCAL + else: + base_path = Path('{USER}') + + log_levels = { + None: logging.NOTSET, + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL, + } + + return { + # File Logging + 'use_log_file': profile_settings['use_log_file'], + 'log_file_path': str(base_path / profile_settings['log_file_path']), + 'log_file_level': log_levels[profile_settings['log_file_level']], + # Console Logging + 'use_log_console': profile_settings['use_log_console'], + 'log_console_level': log_levels[profile_settings['log_console_level']], + } + + +#################### +# - Wheel Downloader +#################### +def download_wheels() -> dict: + with tempfile.NamedTemporaryFile(delete=False) as f_reqlock: + reqlock_str = subprocess.check_output(['uv', 'export', '--no-dev']) + f_reqlock.write(reqlock_str) + reqlock_path = Path(f_reqlock.name) + + for platform, pypi_platform_tags in info.PROJ_SPEC['tool']['bl_ext'][ + 'platforms' + ].items(): + print(f'[{platform}] Downloading Wheels...') + print() + print() + platform_constraints = list( + itertools.chain.from_iterable( + [ + ['--platform', pypi_platform_tag] + for pypi_platform_tag in pypi_platform_tags + ] + ) + ) + subprocess.check_call( + [ + sys.executable, + '-m', + 'pip', + 'download', + '--requirement', + str(reqlock_path), + '--dest', + str(info.PATH_WHEELS), + '--require-hashes', + '--only-binary', + ':all:', + '--python-version', + info.REQ_PYTHON_VERSION, + ] + + platform_constraints + ) + print() + + +#################### +# - Pack Extension to ZIP +#################### +def pack_bl_extension( # noqa: PLR0913 + profile: str, replace_if_exists: bool = False, - remove_after_close: bool = True, ) -> typ.Iterator[Path]: """Context manager exposing a folder as a (temporary) zip file. @@ -72,81 +184,51 @@ def zipped_addon( # noqa: PLR0913 The .zip file is deleted afterwards, unless `remove_after_close` is specified. """ # Delete Existing ZIP (maybe) - if path_addon_zip.is_file(): + if info.PATH_ZIP.is_file(): if replace_if_exists: - msg = 'File already exists where ZIP would be made' + msg = 'File already exists where extension ZIP would be generated ({info.PATH_ZIP})' raise ValueError(msg) - path_addon_zip.unlink() + info.PATH_ZIP.unlink() + + init_settings: dict = generate_init_settings_dict(profile) # Create New ZIP file of the addon directory - with zipfile.ZipFile(path_addon_zip, 'w', zipfile.ZIP_DEFLATED) as f_zip: - # Install Addon Files @ /* - for file_to_zip in path_addon_pkg.rglob('*'): - # Dynamically Alter 'bl_info' in __init__.py - ## This is the only way to propagate ex. version information - if str(file_to_zip.relative_to(path_addon_pkg)) == '__init__.py': - with ( - file_to_zip.open('r') as f_init, - tempfile.NamedTemporaryFile(mode='w') as f_tmp, - ): - initpy = f_init.read() - for ( - to_replace, - replacement, - ) in BL_INFO_REPLACEMENTS.items(): - initpy = initpy.replace(to_replace, replacement) - f_tmp.write(initpy) + with zipfile.ZipFile(info.PATH_ZIP, 'w', zipfile.ZIP_DEFLATED) as f_zip: + # Write Blender Extension Manifest + print('Writing Blender Extension Manifest...') + f_zip.writestr(BL_EXT__MANIFEST_FILENAME, tomli_w.dumps(BL_EXT_MANIFEST)) - # Write to ZIP - f_zip.writestr( - str(file_to_zip.relative_to(path_addon_pkg.parent)), - initpy, - ) - - # Write File to Zip - else: - f_zip.write(file_to_zip, file_to_zip.relative_to(path_addon_pkg.parent)) - - # Install pyproject.toml @ /pyproject.toml of Addon - f_zip.write( - path_pyproject_toml, - str(Path(path_addon_pkg.name) / Path(path_pyproject_toml.name)), - ) - - # Install requirements.lock @ /requirements.txt of Addon - f_zip.write( - path_requirements_lock, - str(Path(path_addon_pkg.name) / Path(path_requirements_lock.name)), - ) - - # Set Initial Log-Level + # Write Init Settings + print('Writing Init Settings...') f_zip.writestr( - str(Path(path_addon_pkg.name) / info.BOOTSTRAP_LOG_LEVEL_FILENAME), - str(initial_log_level), + info.PROJ_SPEC['tool']['bl_ext']['packaging']['init_settings_filename'], + tomli_w.dumps(init_settings), ) + # Install Addon Files @ /* + print('Writing Addon Files Settings...') + for file_to_zip in info.PATH_PKG.rglob('*'): + f_zip.write(file_to_zip, file_to_zip.relative_to(info.PATH_PKG.parent)) + + # Install Wheels @ /wheels/* + print('Writing Wheels...') + for wheel_to_zip in info.PATH_WHEELS.rglob('*'): + f_zip.write(wheel_to_zip, Path('wheels') / wheel_to_zip.name) + # Delete the ZIP - try: - yield path_addon_zip - finally: - if remove_after_close: - path_addon_zip.unlink() + print('Packed Blender Extension!') #################### # - Run Blender w/Clean Addon Reinstall #################### -def main(): - with zipped_addon( - path_addon_pkg=info.PATH_ADDON_PKG, - path_addon_zip=info.PATH_ADDON_ZIP, - path_pyproject_toml=info.PATH_ROOT / 'pyproject.toml', - path_requirements_lock=info.PATH_ROOT / 'requirements.lock', - replace_if_exists=True, - remove_after_close=False, - ): - pass - - if __name__ == '__main__': - main() + if not list(info.PATH_WHEELS.iterdir()) or '--download-wheels' in sys.argv: + download_wheels() + + profile = sys.argv[1] + if sys.argv[1] in ['dev', 'release', 'release-debug']: + pack_bl_extension(profile) + else: + msg = f'Packaging profile "{profile}" is invalid. Refer to source of pack.py for more information' + raise ValueError(msg) diff --git a/uv.lock b/uv.lock index 3d55642..3ad397e 100644 --- a/uv.lock +++ b/uv.lock @@ -870,7 +870,7 @@ requires-dist = [ { name = "pydantic-tensor", specifier = ">=0.2" }, { name = "rich", specifier = ">=13.8" }, { name = "rtree", specifier = "==1.3.*" }, - { name = "scipy", specifier = "==1.13.*" }, + { name = "scipy", specifier = "==1.14.*" }, { name = "seaborn", extras = ["stats"], specifier = ">=0.13" }, { name = "sympy", specifier = "==1.13.*" }, { name = "tidy3d", specifier = "==2.7.*" }, @@ -1324,25 +1324,37 @@ wheels = [ [[package]] name = "scipy" -version = "1.13.1" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720 } +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805 }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687 }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638 }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931 }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145 }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227 }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301 }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348 }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062 }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311 }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493 }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 }, + { url = "https://files.pythonhosted.org/packages/b2/ab/070ccfabe870d9f105b04aee1e2860520460ef7ca0213172abfe871463b9/scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", size = 39076999 }, + { url = "https://files.pythonhosted.org/packages/a7/c5/02ac82f9bb8f70818099df7e86c3ad28dae64e1347b421d8e3adf26acab6/scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", size = 29894570 }, + { url = "https://files.pythonhosted.org/packages/ed/05/7f03e680cc5249c4f96c9e4e845acde08eb1aee5bc216eff8a089baa4ddb/scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", size = 23103567 }, + { url = "https://files.pythonhosted.org/packages/5e/fc/9f1413bef53171f379d786aabc104d4abeea48ee84c553a3e3d8c9f96a9c/scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", size = 25499102 }, + { url = "https://files.pythonhosted.org/packages/c2/4b/b44bee3c2ddc316b0159b3d87a3d467ef8d7edfd525e6f7364a62cd87d90/scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", size = 35586346 }, + { url = "https://files.pythonhosted.org/packages/93/6b/701776d4bd6bdd9b629c387b5140f006185bd8ddea16788a44434376b98f/scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2", size = 41165244 }, + { url = "https://files.pythonhosted.org/packages/06/57/e6aa6f55729a8f245d8a6984f2855696c5992113a5dc789065020f8be753/scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", size = 42817917 }, + { url = "https://files.pythonhosted.org/packages/ea/c2/5ecadc5fcccefaece775feadcd795060adf5c3b29a883bff0e678cfe89af/scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", size = 44781033 }, + { url = "https://files.pythonhosted.org/packages/c0/04/2bdacc8ac6387b15db6faa40295f8bd25eccf33f1f13e68a72dc3c60a99e/scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", size = 39128781 }, + { url = "https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", size = 29939542 }, + { url = "https://files.pythonhosted.org/packages/66/67/6ef192e0e4d77b20cc33a01e743b00bc9e68fb83b88e06e636d2619a8767/scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", size = 23148375 }, + { url = "https://files.pythonhosted.org/packages/f6/32/3a6dedd51d68eb7b8e7dc7947d5d841bcb699f1bf4463639554986f4d782/scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", size = 25578573 }, + { url = "https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", size = 35319299 }, + { url = "https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", size = 40849331 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/06f72bc9187840f1c99e1a8750aad4216fc7dfdd7df46e6280add14b4822/scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", size = 42544049 }, + { url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 }, + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, ] [[package]]