refactor: Generate extension semantically

uv-refactor
Sofus Albert Høgsbro Rose 2024-09-26 13:33:25 +02:00
parent bac330ab2f
commit e6ada5028d
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
12 changed files with 350 additions and 797 deletions

View File

@ -83,20 +83,6 @@ from .nodeps.utils import pydeps # noqa: E402
log = simple_logger.get(__name__) 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 # - Load and Register Addon

View File

@ -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 <contact@oscillode.io>"
# 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__/"
# ]

View File

@ -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

View File

@ -5,11 +5,14 @@ description = "Real-time design and visualization of Maxwell simulations in Blen
authors = [ authors = [
{ name = "Sofus Albert Høgsbro Rose", email = "oscillode@sofusrose.com" } { name = "Sofus Albert Høgsbro Rose", email = "oscillode@sofusrose.com" }
] ]
maintainers = [
{ name = "Sofus Albert Høgsbro Rose", email = "oscillode@sofusrose.com" }
]
dependencies = [ dependencies = [
"tidy3d==2.7.*", "tidy3d==2.7.*",
"pydantic==2.9.*", "pydantic==2.9.*",
"sympy==1.13.*", "sympy==1.13.*",
"scipy==1.13.*", "scipy==1.14.*",
"trimesh==4.4.*", "trimesh==4.4.*",
"networkx==3.3.*", "networkx==3.3.*",
"rich>=13.8.*", "rich>=13.8.*",
@ -28,6 +31,64 @@ readme = "README.md"
requires-python = "~= 3.11" requires-python = "~= 3.11"
license = { text = "AGPL-3.0-or-later" } 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 # - Tooling: Rye
#################### ####################
@ -53,7 +114,6 @@ package = false
[tool.ruff] [tool.ruff]
target-version = "py311" target-version = "py311"
line-length = 88 line-length = 88
pycodestyle.max-doc-length = 120
[tool.ruff.lint] [tool.ruff.lint]
task-tags = ["TODO"] task-tags = ["TODO"]
@ -128,6 +188,7 @@ ignore = [
extend-immutable-calls = [] extend-immutable-calls = []
[tool.ruff.lint.pycodestyle] [tool.ruff.lint.pycodestyle]
max-doc-length = 120
ignore-overlong-task-comments = true ignore-overlong-task-comments = true
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]

View File

@ -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 <http://www.gnu.org/licenses/>.
# 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 <http://www.gnu.org/licenses/>.
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)

67
scripts/bl_init.py 100644
View File

@ -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 <http://www.gnu.org/licenses/>.
# 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 <http://www.gnu.org/licenses/>.
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,
#
# )

View File

@ -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 <http://www.gnu.org/licenses/>.
# 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 <http://www.gnu.org/licenses/>.
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
# 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 <http://www.gnu.org/licenses/>.
# 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
)

View File

@ -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 <http://www.gnu.org/licenses/>.
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',
]
)

View File

@ -30,68 +30,52 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import tomllib import tomllib
from pathlib import Path 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 # - Basic
####################
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
#################### ####################
PATH_ROOT = Path(__file__).resolve().parent.parent
with (PATH_ROOT / 'pyproject.toml').open('rb') as f: with (PATH_ROOT / 'pyproject.toml').open('rb') as f:
PROJ_SPEC = tomllib.load(f) PROJ_SPEC = tomllib.load(f)
ADDON_NAME = PROJ_SPEC['project']['name'] REQ_PYTHON_VERSION = PROJ_SPEC['project']['requires-python'].replace('~= ', '')
ADDON_VERSION = PROJ_SPEC['project']['version']
#################### ####################
# - Packaging Information # - Paths
#################### ####################
PATH_ADDON_PKG = PATH_ROOT / 'src' / ADDON_NAME def proc_path(p_str: str) -> Path:
PATH_ADDON_ZIP = PATH_ROOT / 'build' / (ADDON_NAME + '__' + ADDON_VERSION + '.zip') return PATH_ROOT / Path(*p_str.split('/'))
PATH_ADDON_BLEND_STARTER = PATH_ADDON_PKG / 'blenders' / 'starter.blend'
# Set Bootstrap Log Level PATH_PKG = PATH_ROOT / PROJ_SPEC['project']['name']
## This will be the log-level of both console and file logs, at first...
## ...until the addon preferences have been loaded. PATH_DEV = PATH_ROOT / 'dev'
BOOTSTRAP_LOG_LEVEL = logging.DEBUG PATH_DEV.mkdir(exist_ok=True)
BOOTSTRAP_LOG_LEVEL_FILENAME = '.bootstrap_log_level'
# 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' # Compute Current ZIP to Build
## The concrete path to the file that will be packed and installed.
PATH_ADDON_DEV_DEPS = PATH_DEV / '.cached-dev-dependencies' PATH_ZIP = PATH_BUILD / (
PATH_ADDON_DEV_CACHE = PATH_DEV / '.dev-addon-cache' PROJ_SPEC['project']['name'] + '__' + PROJ_SPEC['project']['version'] + '.zip'
PATH_ADDON_DEV_DEPS.mkdir(exist_ok=True) )

View File

@ -30,37 +30,149 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import contextlib import contextlib
import logging import logging
import tempfile import tempfile
import typing as typ import typing as typ
import zipfile import zipfile
from pathlib import Path from pathlib import Path
import itertools
import subprocess
import tomli_w
import info import info
LogLevel: typ.TypeAlias = int LogLevel: typ.TypeAlias = int
_PROJ_VERSION_STR = str( BL_EXT__MANIFEST_FILENAME = 'blender_manifest.toml'
tuple(int(el) for el in info.PROJ_SPEC['project']['version'].split('.')) BL_EXT__SCHEMA_VERSION = '1.0.0'
) BL_EXT__TYPE = 'add-on'
_PROJ_DESC_STR = info.PROJ_SPEC['project']['description']
BL_INFO_REPLACEMENTS = { ####################
"'version': (0, 0, 0),": f"'version': {_PROJ_VERSION_STR},", # - Generate Manifest
"'description': 'Placeholder',": f"'description': '{_PROJ_DESC_STR}',", ####################
# 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 # - Generate Init Settings
path_addon_pkg: Path, ####################
path_addon_zip: Path, def generate_init_settings_dict(profile: str) -> dict:
path_pyproject_toml: Path, profile_settings = info.PROJ_SPEC['tool']['bl_ext']['profiles'][profile]
path_requirements_lock: Path,
initial_log_level: LogLevel = logging.INFO, 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, replace_if_exists: bool = False,
remove_after_close: bool = True,
) -> typ.Iterator[Path]: ) -> typ.Iterator[Path]:
"""Context manager exposing a folder as a (temporary) zip file. """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. The .zip file is deleted afterwards, unless `remove_after_close` is specified.
""" """
# Delete Existing ZIP (maybe) # Delete Existing ZIP (maybe)
if path_addon_zip.is_file(): if info.PATH_ZIP.is_file():
if replace_if_exists: 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) 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 # Create New ZIP file of the addon directory
with zipfile.ZipFile(path_addon_zip, 'w', zipfile.ZIP_DEFLATED) as f_zip: with zipfile.ZipFile(info.PATH_ZIP, 'w', zipfile.ZIP_DEFLATED) as f_zip:
# Install Addon Files @ /* # Write Blender Extension Manifest
for file_to_zip in path_addon_pkg.rglob('*'): print('Writing Blender Extension Manifest...')
# Dynamically Alter 'bl_info' in __init__.py f_zip.writestr(BL_EXT__MANIFEST_FILENAME, tomli_w.dumps(BL_EXT_MANIFEST))
## 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)
# Write to ZIP # Write Init Settings
f_zip.writestr( print('Writing Init Settings...')
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
f_zip.writestr( f_zip.writestr(
str(Path(path_addon_pkg.name) / info.BOOTSTRAP_LOG_LEVEL_FILENAME), info.PROJ_SPEC['tool']['bl_ext']['packaging']['init_settings_filename'],
str(initial_log_level), 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 # Delete the ZIP
try: print('Packed Blender Extension!')
yield path_addon_zip
finally:
if remove_after_close:
path_addon_zip.unlink()
#################### ####################
# - Run Blender w/Clean Addon Reinstall # - 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__': 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)

42
uv.lock
View File

@ -870,7 +870,7 @@ requires-dist = [
{ name = "pydantic-tensor", specifier = ">=0.2" }, { name = "pydantic-tensor", specifier = ">=0.2" },
{ name = "rich", specifier = ">=13.8" }, { name = "rich", specifier = ">=13.8" },
{ name = "rtree", specifier = "==1.3.*" }, { name = "rtree", specifier = "==1.3.*" },
{ name = "scipy", specifier = "==1.13.*" }, { name = "scipy", specifier = "==1.14.*" },
{ name = "seaborn", extras = ["stats"], specifier = ">=0.13" }, { name = "seaborn", extras = ["stats"], specifier = ">=0.13" },
{ name = "sympy", specifier = "==1.13.*" }, { name = "sympy", specifier = "==1.13.*" },
{ name = "tidy3d", specifier = "==2.7.*" }, { name = "tidy3d", specifier = "==2.7.*" },
@ -1324,25 +1324,37 @@ wheels = [
[[package]] [[package]]
name = "scipy" name = "scipy"
version = "1.13.1" version = "1.14.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy" }, { 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 = [ 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/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/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/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/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/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/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/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/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/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/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227 }, { 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/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/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/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/ea/c2/5ecadc5fcccefaece775feadcd795060adf5c3b29a883bff0e678cfe89af/scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", size = 44781033 },
{ 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/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/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/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/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/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/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 }, { 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]] [[package]]