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__)
####################
# - 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

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 = [
{ 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]

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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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'
)

View File

@ -30,37 +30,149 @@
# 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 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:
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 Init Settings
print('Writing Init Settings...')
f_zip.writestr(
info.PROJ_SPEC['tool']['bl_ext']['packaging']['init_settings_filename'],
tomli_w.dumps(init_settings),
)
# 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)
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))
# 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
f_zip.writestr(
str(Path(path_addon_pkg.name) / info.BOOTSTRAP_LOG_LEVEL_FILENAME),
str(initial_log_level),
)
# 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)

42
uv.lock
View File

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