From 3409e41680c0bb601be77bdca71ff8c371b8e448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Mon, 13 Jan 2025 18:30:57 +0100 Subject: [PATCH] feat: Working app! --- .editorconfig | 16 + .gitignore | 10 + .python-version | 1 + README.md | 9 + blext/__init__.py | 1 + blext/__main__.py | 9 + blext/blender_python/bl_init.py | 87 +++ blext/cli.py | 275 +++++++ blext/finders.py | 108 +++ blext/pack.py | 132 ++++ blext/spec.py | 501 +++++++++++++ blext/supported.py | 55 ++ blext/wheels.py | 189 +++++ examples/simple/.gitignore | 163 ++++ examples/simple/.python-version | 1 + examples/simple/README.md | 2 + .../simple/blext_simple_example/__init__.py | 83 +++ .../contracts/__init__.py | 66 ++ .../blext_simple_example/contracts/addon.py | 49 ++ .../blext_simple_example/contracts/bl.py | 283 +++++++ .../contracts/bl_handlers.py | 115 +++ .../contracts/bl_keymap.py | 44 ++ .../blext_simple_example/contracts/icons.py | 7 + .../contracts/operator_types.py | 11 + .../contracts/panel_types.py | 13 + .../operators/__init__.py | 20 + .../operators/simple_operator.py | 34 + .../blext_simple_example/panels/__init__.py | 20 + .../panels/simple_panel.py | 52 ++ .../blext_simple_example/preferences.py | 185 +++++ .../blext_simple_example/registration.py | 143 ++++ .../blext_simple_example/services/__init__.py | 0 .../services/init_settings.py | 58 ++ .../blext_simple_example/utils/__init__.py | 0 .../blext_simple_example/utils/logger.py | 196 +++++ .../utils/staticproperty.py | 30 + examples/simple/pyproject.toml | 200 +++++ examples/simple/uv.lock | 298 ++++++++ pyproject.toml | 201 +++++ tests/__init__.py | 3 + tests/context.py | 7 + tests/test_cli.py | 15 + uv.lock | 697 ++++++++++++++++++ 43 files changed, 4389 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 blext/__init__.py create mode 100644 blext/__main__.py create mode 100644 blext/blender_python/bl_init.py create mode 100644 blext/cli.py create mode 100644 blext/finders.py create mode 100644 blext/pack.py create mode 100644 blext/spec.py create mode 100644 blext/supported.py create mode 100644 blext/wheels.py create mode 100644 examples/simple/.gitignore create mode 100644 examples/simple/.python-version create mode 100644 examples/simple/README.md create mode 100644 examples/simple/blext_simple_example/__init__.py create mode 100644 examples/simple/blext_simple_example/contracts/__init__.py create mode 100644 examples/simple/blext_simple_example/contracts/addon.py create mode 100644 examples/simple/blext_simple_example/contracts/bl.py create mode 100644 examples/simple/blext_simple_example/contracts/bl_handlers.py create mode 100644 examples/simple/blext_simple_example/contracts/bl_keymap.py create mode 100644 examples/simple/blext_simple_example/contracts/icons.py create mode 100644 examples/simple/blext_simple_example/contracts/operator_types.py create mode 100644 examples/simple/blext_simple_example/contracts/panel_types.py create mode 100644 examples/simple/blext_simple_example/operators/__init__.py create mode 100644 examples/simple/blext_simple_example/operators/simple_operator.py create mode 100644 examples/simple/blext_simple_example/panels/__init__.py create mode 100644 examples/simple/blext_simple_example/panels/simple_panel.py create mode 100644 examples/simple/blext_simple_example/preferences.py create mode 100644 examples/simple/blext_simple_example/registration.py create mode 100644 examples/simple/blext_simple_example/services/__init__.py create mode 100644 examples/simple/blext_simple_example/services/init_settings.py create mode 100644 examples/simple/blext_simple_example/utils/__init__.py create mode 100644 examples/simple/blext_simple_example/utils/logger.py create mode 100644 examples/simple/blext_simple_example/utils/staticproperty.py create mode 100644 examples/simple/pyproject.toml create mode 100644 examples/simple/uv.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/context.py create mode 100644 tests/test_cli.py create mode 100644 uv.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bdf1073 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 + +[*.yaml] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4aef66 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# `blext`: Effortlessly Manage Blender Extension Projects +Documentation TBD. +Quite experimental for the moment. + +For now just do + +`uvx blext` + +for the help text. diff --git a/blext/__init__.py b/blext/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/blext/__init__.py @@ -0,0 +1 @@ + diff --git a/blext/__main__.py b/blext/__main__.py new file mode 100644 index 0000000..3133aac --- /dev/null +++ b/blext/__main__.py @@ -0,0 +1,9 @@ +"""Runs when the package is executed. + +Executes the `typer` app defined in `cli.py`. +""" + +if __name__ == '__main__': + from blext.cli import app + + app() diff --git a/blext/blender_python/bl_init.py b/blext/blender_python/bl_init.py new file mode 100644 index 0000000..01ad62c --- /dev/null +++ b/blext/blender_python/bl_init.py @@ -0,0 +1,87 @@ +import os +import sys +from pathlib import Path + +import bpy + +#################### +# - Constants +#################### +# Addon Name +if os.environ.get('BLEXT_ADDON_NAME') is not None: + BLEXT_ADDON_NAME = os.environ['BLEXT_ADDON_NAME'] +else: + msg = "The env var 'BLEXT_ADDON_NAME' must be available, but it is not" + raise ValueError(msg) + +# Zip Path +if os.environ.get('BLEXT_ZIP_PATH') is not None: + BLEXT_ZIP_PATH = Path(os.environ['BLEXT_ZIP_PATH']) +else: + msg = "The env var 'BLEXT_ZIP_PATH' must be available, but it is not" + raise ValueError(msg) + +# Local Path +if os.environ.get('BLEXT_ZIP_PATH') is not None: + BLEXT_LOCAL_PATH = Path(os.environ['BLEXT_LOCAL_PATH']) +else: + msg = "The env var 'BLEXT_LOCAL_PATH' must be available, but it is not" + raise ValueError(msg) + +BLEXT_DEV_REPO_NAME = 'blext_dev_repo' + + +#################### +# - main() +#################### +if __name__ == '__main__': + # Suppress Splash Screen + ## - It just gets in the way. + bpy.context.preferences.view.show_splash = False + + # Check for Local Dev Repo + ## - Create if non-existant. + if BLEXT_DEV_REPO_NAME not in { + repo.name for repo in bpy.context.preferences.extensions.repos + }: + bpy.ops.preferences.extension_repo_add( + name=BLEXT_DEV_REPO_NAME, + remote_url='', + use_access_token=False, + use_sync_on_startup=False, + # use_custom_directory=True, + # custom_directory=str(BLEXT_LOCAL_PATH), + type='LOCAL', + ) + + # Get the Repository Index of Local Dev Repo + dev_repo_idx = next( + repo_idx + for repo_idx, repo in enumerate(bpy.context.preferences.extensions.repos) + if repo.name == BLEXT_DEV_REPO_NAME + ) + dev_repo_module = next( + repo.module + for repo_idx, repo in enumerate(bpy.context.preferences.extensions.repos) + if repo.name == BLEXT_DEV_REPO_NAME + ) + + # Uninstall Existing Addon + if BLEXT_ADDON_NAME in bpy.context.preferences.addons.keys(): # noqa: SIM118 + bpy.ops.extensions.package_uninstall( + repo_index=dev_repo_idx, + pkg_id=BLEXT_ADDON_NAME, + ) + + # Vacuum sys.modules (remove bl_ext..) + ## - TODO: Start conversation upstream abt. overlapping dependencies. + if f'blext.{BLEXT_DEV_REPO_NAME}.{BLEXT_ADDON_NAME}' in sys.modules: + del sys.modules[BLEXT_ADDON_NAME] + + # Install New Extension + bpy.ops.extensions.package_install_files( + #'INVOKE_DEFAULT', + repo=dev_repo_module, + filepath=str(BLEXT_ZIP_PATH), + enable_on_install=True, + ) diff --git a/blext/cli.py b/blext/cli.py new file mode 100644 index 0000000..d2e1445 --- /dev/null +++ b/blext/cli.py @@ -0,0 +1,275 @@ +"""A `typer`-based command line interface for the `blext` project manager.""" + +import os +import subprocess +import sys +import typing as typ +from pathlib import Path + +import cyclopts +import rich + +from . import finders, pack, spec, supported, wheels + +console = rich.console.Console() +app = cyclopts.App( + name='blext', + help='blext simplifies the development and management of Blender extensions.', + help_format='markdown', +) + +#################### +# - Constants +#################### +PATH_BL_INIT_PY: Path = ( + Path(__file__).resolve().parent / 'blender_python' / 'bl_init.py' +) + + +#################### +# - Command: Build +#################### +@app.command() +def build( + bl_platform: supported.BLPlatform | None = None, + proj_path: Path | None = None, + release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release, + _return_blext_spec: typ.Annotated[bool, cyclopts.Parameter(show=False)] = False, +) -> spec.BLExtSpec | None: + """[Build] the extension to an installable `.zip` file. + + Parameters: + bl_platform: The Blender platform to build the extension for. + proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension. + release_profile: The release profile to bake into the extension. + """ + # Parse CLI + if bl_platform is None: + bl_platform = finders.detect_local_blplatform() + path_proj_spec = finders.find_proj_spec(proj_path) + + # Parse Blender Extension Specification + blext_spec = spec.BLExtSpec.from_proj_spec( + path_proj_spec=path_proj_spec, + release_profile=release_profile, + ).constrain_to_bl_platform(bl_platform) + + # Download Wheels + wheels.download_wheels( + blext_spec, + bl_platform=bl_platform, + no_prompt=False, + ) + + # Pack the Blender Extension + pack.pack_bl_extension(blext_spec, overwrite=True) + + # Validate the Blender Extension + console.print() + console.rule('[bold green]Extension Validation') + + console.print('[italic]$ blender --factory-startup --command extension validate') + console.print() + console.rule(characters='--', style='gray') + + blender_exe = finders.find_blender_exe() + bl_process = subprocess.Popen( + [ + blender_exe, + '--factory-startup', ## Temporarily Disable All Addons + '--command', ## Validate an Extension + 'extension', + 'validate', + str(blext_spec.path_zip), + ], + bufsize=0, ## TODO: Check if -1 is OK + env=os.environ, + stdout=sys.stdout, + stderr=sys.stderr, + ) + return_code = bl_process.wait() + + console.rule(characters='--', style='gray') + console.print() + if return_code == 0: + console.print('[✔] Blender Extension Validation Succeeded') + else: + msg = 'Packed extension did not validate' + raise ValueError(msg) + + if _return_blext_spec: + return blext_spec + return None + + +#################### +# - Command: Dev +#################### +@app.command() +def dev( + proj_path: Path | None = None, +) -> None: + """Launch the local [dev] extension w/Blender.""" + # Build the Extension + blext_spec = build( + bl_platform=None, + proj_path=proj_path, + release_profile=supported.ReleaseProfile.Dev, + _return_blext_spec=True, + ) + + # Find Blender + blender_exe = finders.find_blender_exe() + + # Launch Blender + ## - Same as running 'blender --python ./blender_scripts/bl_init.py' + console.print() + console.rule('[bold green]Running Blender') + console.print('[italic]$ blender --factory-startup --python .../bl_init.py') + console.print() + console.rule(characters='--', style='gray') + + bl_process = subprocess.Popen( + [ + blender_exe, + '--python', + str(PATH_BL_INIT_PY), + '--factory-startup', ## Temporarily Disable All Addons + ], + bufsize=0, ## TODO: Check if -1 is OK + env=os.environ + | { + 'BLEXT_ADDON_NAME': blext_spec.id, + 'BLEXT_ZIP_PATH': str(blext_spec.path_zip), + 'BLEXT_LOCAL_PATH': str(blext_spec.path_local), + }, + stdout=sys.stdout, + stderr=sys.stderr, + ) + bl_process.wait() + + +#################### +# - Command: Show +#################### +app_show = cyclopts.App(name='show', help='[Show] information about the extension.') +app.command(app_show) + + +#################### +# - Command: Show Spec +#################### +@app_show.command(name='spec', group='Information') +def show_spec( + bl_platform: supported.BLPlatform | None = None, + proj_path: Path | None = None, + release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release, +) -> None: + """Print the complete extension specification. + + Parameters: + bl_platform: The Blender platform to build the extension for. + proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension. + release_profile: The release profile to bake into the extension. + """ + # Parse BLExtSpec + path_proj_spec = finders.find_proj_spec(proj_path) + blext_spec = spec.BLExtSpec.from_proj_spec( + path_proj_spec=path_proj_spec, + release_profile=release_profile, + ) + + # Constrain BLExtSpec to BLPlatform + if bl_platform is not None: + blext_spec = blext_spec.constrain_to_bl_platform(bl_platform) + + # Show BLExtSpec + rich.print(blext_spec) + + +#################### +# - Command: Show Manifest +#################### +@app_show.command(name='bl_manifest', group='Information') +def show_bl_manifest( + bl_platform: supported.BLPlatform | None = None, + proj_path: Path | None = None, + release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release, +) -> None: + """Print the generated Blender extension manifest. + + Parameters: + bl_platform: The Blender platform to build the extension for. + proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension. + release_profile: The release profile to bake into the extension. + """ + # Parse CLI + if bl_platform is None: + bl_platform = finders.detect_local_blplatform() + path_proj_spec = finders.find_proj_spec(proj_path) + + # Parse Blender Extension Specification + blext_spec = spec.BLExtSpec.from_proj_spec( + path_proj_spec=path_proj_spec, + release_profile=release_profile, + ) + + # [Maybe] Constrain BLExtSpec to BLPlatform + if bl_platform is not None: + blext_spec = blext_spec.constrain_to_bl_platform(bl_platform) + + # Show BLExtSpec + rich.print(blext_spec.manifest_str) + + +@app_show.command(name='init_settings', group='Information') +def show_init_settings( + bl_platform: supported.BLPlatform | None = None, + proj_path: Path | None = None, + release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release, +) -> None: + """Print the generated initial settings for the release profile. + + Parameters: + bl_platform: The Blender platform to build the extension for. + proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension. + release_profile: The release profile to bake into the extension. + """ + # Parse CLI + if bl_platform is None: + bl_platform = finders.detect_local_blplatform() + path_proj_spec = finders.find_proj_spec(proj_path) + + # Parse Blender Extension Specification + blext_spec = spec.BLExtSpec.from_proj_spec( + path_proj_spec=path_proj_spec, + release_profile=release_profile, + ) + + # [Maybe] Constrain BLExtSpec to BLPlatform + if bl_platform is not None: + blext_spec = blext_spec.constrain_to_bl_platform(bl_platform) + + # Show BLExtSpec + rich.print(blext_spec.init_settings_str) + + +#################### +# - Command: Show Path to Blender +#################### +@app_show.command(name='path_blender', group='Paths') +def show_path_blender() -> None: + """Display the found path to the Blender executable.""" + blender_exe = finders.find_blender_exe() + + rich.print(blender_exe) + + +#################### +# - Command: Help and Version +#################### +app['--help'].group = 'Debug' +app['--version'].group = 'Debug' + +app_show['--help'].group = 'Debug' +app_show['--version'].group = 'Debug' diff --git a/blext/finders.py b/blext/finders.py new file mode 100644 index 0000000..36904aa --- /dev/null +++ b/blext/finders.py @@ -0,0 +1,108 @@ +"""Tools for finding common information and files using platform-specific methods.""" + +import platform +import shutil +from pathlib import Path + +from .supported import BLPlatform + + +def detect_local_blplatform() -> BLPlatform: + """Deduce the local Blender platform from `platform.system()` and `platform.machine()`. + + References: + Architecture Strings on Linus: + + Warnings: + This method is mostly untested, especially on Windows. + + Returns: + A local operating system supported by Blender, conforming to the format expected by Blender's extension manifest. + + Raises: + ValueError: If a Blender-supported operating system could not be detected locally. + """ + platform_system = platform.system().lower() + platform_machine = platform.machine().lower() + + match (platform_system, platform_machine): + case ('linux', 'x86_64' | 'amd64'): + return BLPlatform.linux_x64 + case ('linux', arch) if arch.startswith(('aarch64', 'arm')): + return BLPlatform.linux_arm64 + case ('darwin', 'x86_64' | 'amd64'): + return BLPlatform.macos_x64 + case ('darwin', arch) if arch.startswith(('aarch64', 'arm')): + return BLPlatform.macos_arm64 + case ('linux', 'x86_64' | 'amd64'): + return BLPlatform.windows_x64 + case ('linux', arch) if arch.startswith(('aarch64', 'arm')): + return BLPlatform.windows_arm64 + + msg = "Could not detect a local operating system supported by Blender from 'platform.system(), platform.machine() = {platform_system}, {platform_machine}'" + raise ValueError(msg) + + +def find_proj_spec(proj_path: Path | None) -> Path: + """Locate the project specification using the given hint. + + Parameters: + proj_path: Search for a `pyproject.toml` project specification at this path. + When `None`, search in the current working directory. + """ + if proj_path is None: + path_proj_spec = Path.cwd() / 'pyproject.toml' + if not path_proj_spec.is_file(): + symptom = ( + 'does not exist' if not path_proj_spec.exists() else 'is not a file' + ) + msg = "Couldn't find 'pyproject.toml' in the current working directory" + + elif proj_path.is_dir() and (proj_path / 'pyproject.toml').is_file(): + path_proj_spec = proj_path / 'pyproject.toml' + + elif not proj_path.is_file(): + symptom = 'does not exist' if not proj_path.exists() else 'is not a file' + msg = f"Provided project specification {symptom} (tried to load '{proj_path}')" + raise ValueError(msg) + + return path_proj_spec.resolve() + + +def find_blender_exe() -> str: + """Locate the Blender executable, using the current platform as a hint. + + Parameters: + os: The currently supported operating system. + + Returns: + Absolute path to a valid Blender executable, as a string. + """ + bl_platform = detect_local_blplatform() + match bl_platform: + case BLPlatform.linux_x64: + blender_exe = shutil.which('blender') + if blender_exe is not None: + return blender_exe + + msg = "Couldn't find executable command 'blender' on the system PATH. Is it installed?" + raise ValueError(msg) + + case BLPlatform.macos_arm64: + blender_exe = '/Applications/Blender.app/Contents/MacOS/Blender' + if Path(blender_exe).is_file(): + return blender_exe + + msg = f"Couldn't find Blender executable at standard path. Is it installed? (searched '{blender_exe}')" + raise ValueError(msg) + + case BLPlatform.windows_x64: + blender_exe = shutil.which('blender.exe') + if blender_exe is not None: + return blender_exe + + msg = "Couldn't find executable command 'blender.exe' on the system PATH. Is it installed?" + raise ValueError(msg) + + msg = f"Could not detect a Blender executable for the current Blender platform '{bl_platform}'." + raise ValueError(msg) diff --git a/blext/pack.py b/blext/pack.py new file mode 100644 index 0000000..0f7aa37 --- /dev/null +++ b/blext/pack.py @@ -0,0 +1,132 @@ +"""Contains tools and procedures that deterministically and reliably packages the Blender extension. + +This involves parsing and validating the plugin configuration from `pyproject.toml`, generating the extension manifest, downloading the correct platform-specific binary wheels for distribution, and zipping it all up together. +""" + +import shutil +import zipfile +from pathlib import Path + +import rich +import rich.progress +import rich.prompt + +from blext import spec + +console = rich.console.Console() + + +#################### +# - Pack Extension to ZIP +#################### +def prepack_bl_extension(blext_spec: spec.BLExtSpec) -> None: + """Prepare a `.zip` file containing all wheels, but no code. + + Since the wheels tend to be the slowest part of packing an extension, a two-step process allows reusing a base `.zip` file that already contains required wheels. + """ + # Overwrite + if blext_spec.path_zip_prepack.is_file(): + blext_spec.path_zip_prepack.unlink() + + console.print() + console.rule('[bold green]Pre-Packing Extension') + with zipfile.ZipFile( + blext_spec.path_zip_prepack, 'w', zipfile.ZIP_DEFLATED + ) as f_zip: + # Install Wheels @ /wheels/* + ## Setup UI + total_wheel_size = sum( + f.stat().st_size for f in blext_spec.path_wheels.rglob('*') if f.is_file() + ) + progress = rich.progress.Progress( + rich.progress.TextColumn( + 'Writing Wheel: {task.description}...', + table_column=rich.progress.Column(ratio=2), + ), + rich.progress.BarColumn( + bar_width=None, + table_column=rich.progress.Column(ratio=2), + ), + expand=True, + ) + progress_task = progress.add_task('Writing Wheels...', total=total_wheel_size) + + ## Write Wheels + with rich.progress.Live(progress, console=console, transient=True) as live: + for wheel_to_zip in blext_spec.path_wheels.rglob('*.whl'): + f_zip.write(wheel_to_zip, Path('wheels') / wheel_to_zip.name) + progress.update( + progress_task, + description=wheel_to_zip.name, + advance=wheel_to_zip.stat().st_size, + ) + live.refresh() + + console.print('[✔] Wrote Wheels') + + +def pack_bl_extension( + blext_spec: spec.BLExtSpec, + *, + force_prepack: bool = False, + overwrite: bool = False, +) -> None: + """Pack all files needed by a Blender extension, into an installable `.zip`. + + Configuration data is sourced from `paths`, which in turns sources much of its user-facing configuration from `pyproject.toml`. + + Parameters: + profile: Selects a predefined set of initial extension settings from a `[tool.bl_ext.profiles]` table in `pyproject.toml`. + os: The operating system to pack the extension for. + force_prepack: Force pre-packing all wheels into the zip file. + overwrite: Replace the zip file if it already exists. + """ + path_zip = blext_spec.path_zip + if force_prepack or not blext_spec.path_zip_prepack.is_file(): + prepack_bl_extension(blext_spec) + + # Overwrite Existing ZIP + if path_zip.is_file(): + if not overwrite: + msg = f"File already exists where extension ZIP would be generated at '{path_zip}'" + raise ValueError(msg) + path_zip.unlink() + + # Pack Extension + console.print() + console.rule('[bold green]Packing Extension') + console.print(f'[italic]Writing to: {path_zip.parent}') + console.print() + + ## Copy Prepacked ZIP and Finish Packing + with console.status('Copying Prepacked ZIP...', spinner='dots'): + shutil.copyfile(blext_spec.path_zip_prepack, path_zip) + console.print('[✔] Copied Prepacked Extension ZIP') + + with zipfile.ZipFile(path_zip, 'a', zipfile.ZIP_DEFLATED) as f_zip: + # Write Blender Extension Manifest @ /blender_manifest.toml + with console.status('Writing Extension Manifest...', spinner='dots'): + f_zip.writestr( + blext_spec.manifest_filename, + blext_spec.manifest_str, + ) + console.print('[✔] Wrote Extension Manifest') + + # Write Init Settings @ /init_settings.toml + with console.status('Writing Init Settings...', spinner='dots'): + f_zip.writestr( + blext_spec.init_settings_filename, + blext_spec.init_settings_str, + ) + console.print('[✔] Wrote Init Settings') + + # Install Addon Files @ /* + with console.status('Writing Addon Files...', spinner='dots'): + for file_to_zip in blext_spec.path_pkg.rglob('*'): + f_zip.write( + file_to_zip, + file_to_zip.relative_to(blext_spec.path_pkg), + ) + console.print('[✔] Wrote Addon Files') + + console.print(f'[✔] Finished Writing: "{path_zip.name}"') diff --git a/blext/spec.py b/blext/spec.py new file mode 100644 index 0000000..51a63eb --- /dev/null +++ b/blext/spec.py @@ -0,0 +1,501 @@ +"""Defines the `BLExtSpec` model.""" + +import functools +import json +import tomllib +import typing as typ +from pathlib import Path + +import pydantic as pyd +import tomli_w + +from . import supported + + +#################### +# - Path Mangling +#################### +def parse_spec_path(path_proj_root: Path, p_str: str) -> Path: + """Convert a project-root-relative '/'-delimited string path to an absolute, cross-platform path. + + This allows for cross-platform path specification, while retaining normalized use of '/' in configuration. + + Args: + path_proj_root: The path to the project root directory. + p_str: The '/'-delimited string denoting a path relative to the project root. + + Returns: + The full absolute path to use when ex. packaging. + """ + return path_proj_root / Path(*p_str.split('/')) + + +#################### +# - Types +#################### +# class BLExtSpec(pyd.BaseModel, frozen=True): ## TODO: FrozenDict +class BLExtSpec(pyd.BaseModel): + """Completely encapsulates information about the packaging of a Blender extension. + + This model allows `pyproject.toml` to be the single source of truth for a Blender extension project. + Thus, this model is designed to be parsed entirely from a `pyproject.toml` file, and in turn is capable of generating the Blender extension manifest file and more. + + To the extent possible, appropriate standard `pyproject.toml` fields are scraped for information relevant to a Blender extension. + This includes name, version, license, desired dependencies, homepage, and more. + Naturally, many fields are _quite_ specific to Blender extensions, such as Blender version constraints, permissions, and extension tags. + For such options, the `[tool.blext]` section is introduced. + + Attributes: + init_settings_filename: Must be `init_settings.toml`. + use_path_local: Whether to use a local path, instead of a global system path. + Useful for debugging during development. + use_log_file: Whether the extension should default to the use of file logging. + log_file_path: The path to the file log (if enabled). + log_file_level: The file log level to use (if enabled). + use_log_console: Whether the extension should default to the use of console logging. + log_console_level: The console log level to use (if enabled). + schema_version: Must be 1.0.0. + id: Unique identifying name of the extension. + name: Pretty, user-facing name of the extension. + version: The version of the extension. + tagline: Short description of the extension. + maintainer: Primary maintainer of the extension (name and email). + type: Type of extension. + Currently, only `add-on` is supported. + blender_version_min: The minimum version of Blender that this extension supports. + blender_version_max: The maximum version of Blender that this extension supports. + wheels: Relative paths to wheels distributed with this extension. + These should be installed by Blender alongside the extension. + See for more information. + permissions: Permissions required by the extension. + tags: Tags for categorizing the extension. + license: License of the extension's source code. + copyright: Copyright declaration of the extension. + website: Homepage of the extension. + + References: + - + - + """ + + # Base + path_proj_root: Path + req_python_version: str + + # Platform Support + is_universal_blext: bool = True + bl_platform_pypa_tags: dict[str, tuple[str, ...]] + + #################### + # - Packed Filenames + #################### + init_settings_filename: typ.Literal['init_settings.toml'] = 'init_settings.toml' + manifest_filename: typ.Literal['blender_manifest.toml'] = 'blender_manifest.toml' + + #################### + # - Init Settings + #################### + init_schema_version: typ.Literal['0.1.0'] = pyd.Field( + default='0.1.0', serialization_alias='schema_version' + ) + ## TODO: Conform to extension version? + + # Path Locality + use_path_local: bool + + # File Logging + use_log_file: bool + log_file_path: Path + log_file_level: supported.StrLogLevel + + # Console Logging + use_log_console: bool + log_console_level: supported.StrLogLevel + + #################### + # - Extension Manifest + #################### + manifest_schema_version: typ.Literal['1.0.0'] = pyd.Field( + default='1.0.0', serialization_alias='schema_version' + ) + + # Basics + id: str + name: str + version: str + tagline: str + maintainer: str + + ## TODO: Validator on tagline that prohibits ending with punctuation + ## - In fact, alpha-numeric suffix is required. + + # Blender Compatibility + type: typ.Literal['add-on'] = 'add-on' + blender_version_min: str + blender_version_max: str + + # OS/Arch Compatibility + + # 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: dict[ + typ.Literal['files', 'network', 'clipboard', 'camera', 'microphone'], str + ] = {} + + # Addon Tags + tags: tuple[ + typ.Literal[ + '3D View', + 'Add Curve', + 'Add Mesh', + 'Animation', + 'Bake', + 'Camera', + 'Compositing', + 'Development', + 'Game Engine', + 'Geometry Nodes', + 'Grease Pencil', + 'Import-Export', + 'Lighting', + 'Material', + 'Modeling', + 'Mesh', + 'Node', + 'Object', + 'Paint', + 'Pipeline', + 'Physics', + 'Render', + 'Rigging', + 'Scene', + 'Sculpt', + 'Sequencer', + 'System', + 'Text Editor', + 'Tracking', + 'User Interface', + 'UV', + ], + ..., + ] = () + license: tuple[str, ...] + copyright: tuple[str, ...] + website: pyd.HttpUrl | None = None + + #################### + # - Extension Manifest: Computed Properties + #################### + @pyd.computed_field(alias='platforms') # type: ignore[prop-decorator] + @property + def bl_platforms(self) -> frozenset[supported.BLPlatform]: + """Operating systems supported by the extension.""" + return frozenset(self.bl_platform_pypa_tags.keys()) # type: ignore[arg-type] + + @pyd.computed_field # type: ignore[prop-decorator] + @property + def wheels(self) -> tuple[Path, ...]: + """Path to all shipped wheels, relative to the root of the unpacked extension `.zip` file.""" + return tuple( + [ + Path(f'./wheels/{wheel_path.name}') + for wheel_path in self.path_wheels.iterdir() + ] + ) + + #################### + # - Path Properties + #################### + @functools.cached_property + def path_proj_spec(self) -> Path: + """Path to the project `pyproject.toml`.""" + return self.path_proj_root / 'pyproject.toml' + + @functools.cached_property + def path_pkg(self) -> Path: + """Path to the Python package of the extension.""" + return self.path_proj_root / self.id + + @functools.cached_property + def path_dev(self) -> Path: + """Path to the project `dev/` folder, which should not be checked in.""" + path_dev = self.path_proj_root / 'dev' + path_dev.mkdir(exist_ok=True) + return path_dev + + @functools.cached_property + def path_wheels(self) -> Path: + """Path to the project's downloaded wheel cache.""" + path_wheels = self.path_dev / 'wheels' + path_wheels.mkdir(exist_ok=True) + return path_wheels + + @functools.cached_property + def path_prepack(self) -> Path: + """Path to the project's prepack folder, where pre-packed `.zip`s are written to.""" + path_prepack = self.path_dev / 'prepack' + path_prepack.mkdir(exist_ok=True) + return path_prepack + + @functools.cached_property + def path_build(self) -> Path: + """Path to the project's build folder, where extension `.zip`s are written to.""" + path_build = self.path_dev / 'build' + path_build.mkdir(exist_ok=True) + return path_build + + @functools.cached_property + def path_local(self) -> Path: + """Path to the project's build folder, where extension `.zip`s are written to.""" + path_build = self.path_dev / 'build' + path_build.mkdir(exist_ok=True) + return path_build + + @functools.cached_property + def filename_zip(self) -> str: + """Deduce the filename of the `.zip` extension file to build.""" + # Deduce Zip Filename + if self.is_universal_blext: + return f'{self.id}__{self.version}.zip' + + if len(self.bl_platforms) == 1: + only_supported_os = next(iter(self.bl_platforms)) + return f'{self.id}__{self.version}_{only_supported_os}.zip' + + msg = "Cannot deduce filename of non-universal Blender extension when more than one 'BLPlatform' is supported." + raise ValueError(msg) + + @functools.cached_property + def path_zip(self) -> Path: + """Path to the Blender extension `.zip` to build.""" + return self.path_build / self.filename_zip + + @functools.cached_property + def path_zip_prepack(self) -> Path: + """Path to the Blender extension `.zip` to build.""" + return self.path_prepack / self.filename_zip + + #################### + # - Exporters + #################### + @functools.cached_property + def init_settings_str(self) -> str: + """The Blender extension manifest TOML as a string.""" + return tomli_w.dumps( + json.loads( + self.model_dump_json( + include={ + 'init_schema_version', + 'use_path_local', + 'use_log_file', + 'log_file_path', + 'log_file_level', + 'use_log_console', + 'log_console_level', + }, + by_alias=True, + ) + ) + ) + + @functools.cached_property + def manifest_str(self) -> str: + """The Blender extension manifest TOML as a string.""" + return tomli_w.dumps( + json.loads( + self.model_dump_json( + include={ + 'manifest_schema_version', + 'id', + 'name', + 'version', + 'tagline', + 'maintainer', + 'type', + 'blender_version_min', + 'blender_version_max', + 'bl_platforms', + 'wheels', + 'permissions', + 'tags', + 'license', + 'copyright', + } + | ({'website'} if self.website is not None else set()), + by_alias=True, + ) + ) + ) + + #################### + # - Methods + #################### + def constrain_to_bl_platform(self, bl_platform: supported.BLPlatform) -> typ.Self: + """Create a new `BLExtSpec`, which supports only one operating system. + + All PyPa platform tags associated with that operating system will be transferred. + In all other respects, the created `BLExtSpec` will be identical. + + Parameters: + bl_platform: The Blender platform to support exclusively. + + """ + pypa_platform_tags = self.bl_platform_pypa_tags[bl_platform] + return self.model_copy( + update={ + 'bl_platform_pypa_tags': {bl_platform: pypa_platform_tags}, + 'is_universal_blext': False, + }, + deep=True, + ) + + #################### + # - Creation + #################### + @classmethod + def from_proj_spec_dict( + cls, + proj_spec: dict[str, typ.Any], + *, + path_proj_root: Path, + release_profile: supported.ReleaseProfile, + ) -> typ.Self: + """Parse a `BLExtSpec` from a `pyproject.toml` dictionary. + + Args: + proj_spec: The dictionary representation of a `pyproject.toml` file. + + Raises: + ValueError: If the `pyproject.toml` file does not contain the required tables and/or fields. + + """ + # Parse Sections + ## Parse [project] + if proj_spec.get('project') is not None: + project = proj_spec['project'] + else: + msg = "'pyproject.toml' MUST define '[project]' table" + raise ValueError(msg) + + ## Parse [tool.blext] + if ( + proj_spec.get('tool') is not None + or proj_spec['tool'].get('blext') is not None + ): + blext_spec = proj_spec['tool']['blext'] + else: + msg = "'pyproject.toml' MUST define '[tool.blext]' table" + raise ValueError(msg) + + ## Parse [tool.blext.profiles] + if proj_spec['tool']['blext'].get('profiles') is not None: + release_profiles = blext_spec['profiles'] + if release_profile in release_profiles: + release_profile_spec = release_profiles[release_profile] + else: + msg = f"To parse the profile '{release_profile}' from 'pyproject.toml', it MUST be defined as a key in '[tool.blext.profiles]'" + raise ValueError(msg) + + else: + msg = "'pyproject.toml' MUST define '[tool.blext.profiles]'" + raise ValueError(msg) + + # Parse Values + ## Parse project.requires-python + if project.get('requires-python') is not None: + project_requires_python = project['requires-python'].replace('~= ', '') + else: + msg = "'pyproject.toml' MUST define 'project.requires-python'" + raise ValueError(msg) + + ## Parse project.maintainers[0] + if project.get('maintainers') is not None: + first_maintainer = project.get('maintainers')[0] + else: + first_maintainer = {'name': None, 'email': None} + + ## Parse project.license + if ( + project.get('license') is not None + and project['license'].get('text') is not None + ): + _license = project['license']['text'] + else: + msg = "'pyproject.toml' MUST define 'project.license.text'" + raise ValueError(msg) + + ## Parse project.urls.homepage + if ( + project.get('urls') is not None + and project['urls'].get('Homepage') is not None + ): + homepage = project['urls']['Homepage'] + else: + homepage = None + + # Conform to BLExt Specification + return cls( + path_proj_root=path_proj_root, + req_python_version=project_requires_python, + bl_platform_pypa_tags=blext_spec.get('platforms'), + # Path Locality + use_path_local=release_profile_spec.get('use_path_local'), + # File Logging + use_log_file=release_profile_spec.get('use_log_file', False), + log_file_path=release_profile_spec.get('log_file_path', 'addon.log'), + log_file_level=release_profile_spec.get('log_file_level', 'DEBUG'), + # Console Logging + use_log_console=release_profile_spec.get('use_log_console', True), + log_console_level=release_profile_spec.get('log_console_level', 'DEBUG'), + # Basics + id=project.get('name'), + name=blext_spec.get('pretty_name'), + version=project.get('version'), + tagline=project.get('description'), + maintainer=f'{first_maintainer["name"]} <{first_maintainer["email"]}>', + # Blender Compatibility + blender_version_min=blext_spec.get('blender_version_min'), + blender_version_max=blext_spec.get('blender_version_max'), + # Permissions + permissions=blext_spec.get('permissions', {}), + # Addon Tags + tags=blext_spec.get('bl_tags'), + license=(f'SPDX:{_license}',), + copyright=blext_spec.get('copyright'), + website=homepage, + ) + + @classmethod + def from_proj_spec( + cls, + path_proj_spec: Path, + *, + release_profile: supported.ReleaseProfile, + ) -> typ.Self: + """Parse a `BLExtSpec` from a `pyproject.toml` file. + + Args: + path_proj_spec: The path to an appropriately utilized `pyproject.toml` file. + release_profile: The profile to load initial settings for. + + Raises: + ValueError: If the `pyproject.toml` file does not contain the required tables and/or fields. + """ + # Load File + if path_proj_spec.is_file(): + with path_proj_spec.open('rb') as f: + proj_spec = tomllib.load(f) + else: + msg = f"Could not load 'pyproject.toml' at '{path_proj_spec}" + raise ValueError(msg) + + # Parse Extension Specification + return cls.from_proj_spec_dict( + proj_spec, + path_proj_root=path_proj_spec.parent, + release_profile=release_profile, + ) diff --git a/blext/supported.py b/blext/supported.py new file mode 100644 index 0000000..1bf67ba --- /dev/null +++ b/blext/supported.py @@ -0,0 +1,55 @@ +"""Constrained sets of strings denoting some supported set of values. + +String enumerations are used to provide meaningful, editor-friendly choices that are enforced when appropriate ex. in the CLI interface. +""" + +import enum +import logging + + +class BLPlatform(enum.StrEnum): + """Operating systems supported by Blender extensions managed by BLExt. + + Corresponds perfectly to the platforms defined in the Blender Extension Manifest. + """ + + linux_x64 = 'linux-x64' + linux_arm64 = 'linux-arm64' + macos_x64 = 'macos-x64' + macos_arm64 = 'macos-arm64' + windows_x64 = 'windows-x64' + windows_arm64 = 'windows-arm64' + + +class ReleaseProfile(enum.StrEnum): + """Release profiles supported by Blender extensions managed by BLExt.""" + + Test = 'test' + Dev = 'dev' + Release = 'release' + ReleaseDebug = 'release-debug' + + +class StrLogLevel(enum.StrEnum): + """String log-levels corresponding to log-levels in the `logging` stdlib module.""" + + Debug = 'DEBUG' + Info = 'INFO' + Warning = 'WARNING' + Error = 'ERROR' + Critical = 'CRITICAL' + + @property + def log_level(self) -> int: + """The integer corresponding to each string log-level. + + Derived from the `logging` module of the standard library. + """ + SLL = self.__class__ + return { + SLL.Debug: logging.DEBUG, + SLL.Info: logging.INFO, + SLL.Warning: logging.WARNING, + SLL.Error: logging.ERROR, + SLL.Critical: logging.CRITICAL, + }[self] diff --git a/blext/wheels.py b/blext/wheels.py new file mode 100644 index 0000000..87348d8 --- /dev/null +++ b/blext/wheels.py @@ -0,0 +1,189 @@ +import contextlib +import subprocess +import sys +import time +import tomllib + +import pypdl +import rich +import rich.markdown +import rich.progress +import rich.prompt + +from . import pack, spec, supported + +console = rich.console.Console() + +DELAY_DOWNLOAD_PROGRESS = 0.01 + + +#################### +# - Wheel Downloader +#################### +def desired_wheels(blext_spec: spec.BLExtSpec) -> tuple[set[str], dict[str, str]]: + """Deduce the filenames and URLs of the desired wheels.""" + # Run uv Commands + with contextlib.chdir(blext_spec.path_proj_root): + # Generate uv.lock + subprocess.run( + ['uv', 'lock'], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # Retrieve Dependencies + ## - uv must declare which dependencies are NOT dev dependencies. + uv_tree_str = subprocess.check_output( + ['uv', 'tree', '--no-dev', '--locked'], + stderr=subprocess.DEVNULL, + ).decode('utf-8') + + # Find or Create uv.lock + path_uv_lock = blext_spec.path_proj_root / 'uv.lock' + if not path_uv_lock.is_file(): + msg = f"Couldn't find or create 'uv.lock' in project root '{blext_spec.path_proj_root}'" + raise ValueError(msg) + + # Parse for All Wheel Filenames + with path_uv_lock.open('rb') as f: + uv_lockfile = tomllib.load(f) + + wheel_filename_to_url = { + wheel['url'].split('/')[-1]: wheel['url'] + for pkg in uv_lockfile['package'] + for wheel in pkg.get('wheels', []) + if 'wheels' in pkg and pkg['name'] in uv_tree_str + } + wheel_filename_to_url = { + wheel_filename: wheel_url + for wheel_filename, wheel_url in wheel_filename_to_url.items() + if any( + pypa_tag in wheel_filename or 'py3-none-any' in wheel_filename + for bl_platform, pypa_tags in blext_spec.bl_platform_pypa_tags.items() + for pypa_tag in pypa_tags + ) + } + + return ( + set(wheel_filename_to_url.keys()), + wheel_filename_to_url, + ) + + +def download_wheels( + blext_spec: spec.BLExtSpec, + *, + bl_platform: supported.BLPlatform, + no_prompt: bool = False, +) -> None: + """Download universal and binary wheels for all platforms defined in `pyproject.toml`. + + Each blender-supported platform requires specifying a valid list of PyPi platform constraints. + These will be used as an allow-list when deciding which binary wheels may be selected for ex. 'mac'. + + It is recommended to start with the most compatible platform tags, then work one's way up to the newest. + Depending on how old the compatibility should stretch, one may have to omit / manually compile some wheels. + + There is no exhaustive list of valid platform tags - though this should get you started: + - https://stackoverflow.com/questions/49672621/what-are-the-valid-values-for-platform-abi-and-implementation-for-pip-do + - Examine https://pypi.org/project/pillow/#files for some widely-supported tags. + + Args: + delete_existing_wheels: Whether to delete all wheels already in the directory. + This doesn't generally require re-downloading; the pip-cache will generally be hit first. + """ + # Deduce Current | Desired Wheels + wheels_current = {path.name for path in blext_spec.path_wheels.rglob('*.whl')} + wheels_target, wheel_target_urls = desired_wheels(blext_spec) + + # Compute Wheels to Download / Delete + wheels_to_download = wheels_target - wheels_current + wheels_to_delete = wheels_current - wheels_target + + # Delete Superfluous Wheels + if wheels_to_delete: + console.print() + console.rule('[bold green]Wheels to Delete') + console.print(f'[italic]Deleting from: {blext_spec.path_wheels}:') + console.print( + rich.markdown.Markdown( + '\n'.join( + [f'- {wheel_to_delete}' for wheel_to_delete in wheels_to_delete] + ) + ), + ) + console.print() + + if not no_prompt: + if rich.prompt.Confirm.ask('[bold]OK to delete?'): + for wheel_filename in wheels_to_delete: + wheel_path = blext_spec.path_wheels / wheel_filename + if wheel_path.is_file(): + wheel_path.unlink() + else: + msg = f"While deleting superfluous wheels, a wheel path was computed that doesn't point to a valid file: {wheel_path}" + raise RuntimeError(msg) + else: + sys.exit(1) + + # Download Missing Wheels + if wheels_to_download: + console.print() + console.rule('[bold green]Wheels to Download') + console.print(f'[italic]Downloading to: {blext_spec.path_wheels}') + console.print( + rich.markdown.Markdown( + '\n'.join( + [ + f'- {wheel_to_download}' + for wheel_to_download in wheels_to_download + ] + ) + ), + ) + console.print() + + # Start Download + dl = pypdl.Pypdl( + max_concurrent=8, + allow_reuse=False, + ) + future = dl.start( + tasks=[ + { + 'url': wheel_url, + 'file_path': str(blext_spec.path_wheels / wheel_filename), + } + for wheel_filename, wheel_url in wheel_target_urls.items() + ], + retries=3, + display=False, + block=False, + ) + + # Monitor Download w/Progress Bar + with rich.progress.Progress() as progress: + task = progress.add_task('Downloading...', total=100) + + # Wait for Download to Start + while dl.progress is None: + time.sleep(DELAY_DOWNLOAD_PROGRESS) + + # Monitor Download + ## - 99% seems to be the maximum value. + while dl.progress < 99: # noqa: PLR2004 + progress.update(task, completed=dl.progress) + time.sleep(DELAY_DOWNLOAD_PROGRESS) + + # Stop the Download @ 99% + ## - Essentially, we must wait on the future from the started download. + ## - This also merges the downloaded segments and cleans up. + future.result() + + # Finalize Progress Bar to 100% + progress.update(task, completed=100) + + # Prepack ZIP + if wheels_to_delete or wheels_to_download: + pack.prepack_bl_extension(blext_spec) diff --git a/examples/simple/.gitignore b/examples/simple/.gitignore new file mode 100644 index 0000000..c3a6d0e --- /dev/null +++ b/examples/simple/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +dev +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/examples/simple/.python-version b/examples/simple/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/examples/simple/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 0000000..fda1abe --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,2 @@ +# blext-minimal-example +Minimal example of a Blender extension managed with `blext`. diff --git a/examples/simple/blext_simple_example/__init__.py b/examples/simple/blext_simple_example/__init__.py new file mode 100644 index 0000000..b113cf8 --- /dev/null +++ b/examples/simple/blext_simple_example/__init__.py @@ -0,0 +1,83 @@ +"""A visual DSL for electromagnetic simulation design and analysis implemented as a Blender node editor.""" + +from functools import reduce + +from . import contracts as ct +from . import operators, panels, preferences, registration +from .utils import logger + +log = logger.get(__name__) + + +#################### +# - Load and Register Addon +#################### +BL_REGISTER: list[ct.BLClass] = [ + *operators.BL_REGISTER, + *panels.BL_REGISTER, +] + +BL_HANDLERS: ct.BLHandlers = reduce( + lambda a, b: a + b, + [ + operators.BL_HANDLERS, + panels.BL_HANDLERS, + ], + ct.BLHandlers(), +) + +BL_KEYMAP_ITEMS: list[ct.BLKeymapItem] = [ + *operators.BL_KEYMAP_ITEMS, + *panels.BL_KEYMAP_ITEMS, +] + + +#################### +# - Registration +#################### +def register() -> None: + """Implements addon registration in a way that respects the availability of addon preferences and loggers. + + Notes: + Called by Blender when enabling the addon. + + Raises: + RuntimeError: If addon preferences fail to register. + """ + log.info('Registering Addon Preferences: %s', ct.addon.NAME) + registration.register_classes(preferences.BL_REGISTER) + + addon_prefs = ct.addon.prefs() + if addon_prefs is not None: + # Update Loggers + # - This updates existing loggers to use settings defined by preferences. + addon_prefs.on_addon_logging_changed() + + log.info('Registering Addon: %s', ct.addon.NAME) + + registration.register_classes(BL_REGISTER) + registration.register_handlers(BL_HANDLERS) + registration.register_keymaps(BL_KEYMAP_ITEMS) + + log.info('Finished Registration of Addon: %s', ct.addon.NAME) + else: + msg = f"Addon preferences did not register for addon '{ct.addon.NAME}' - something is very wrong!" + raise RuntimeError(msg) + + +def unregister() -> None: + """Unregisters anything that was registered by the addon. + + Notes: + Run by Blender when disabling and/or uninstalling the addon. + + This doesn't clean `sys.modules`. + We rely on the hope that Blender has extension-extension module isolation. + """ + log.info('Starting %s Unregister', ct.addon.NAME) + + registration.unregister_keymaps() + registration.unregister_handlers() + registration.unregister_classes() + + log.info('Finished %s Unregister', ct.addon.NAME) diff --git a/examples/simple/blext_simple_example/contracts/__init__.py b/examples/simple/blext_simple_example/contracts/__init__.py new file mode 100644 index 0000000..95d8966 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/__init__.py @@ -0,0 +1,66 @@ +"""Independent constants and types, which represent a kind of 'social contract' governing communication between all components of the addon.""" + +from . import addon +from .bl import ( + BLClass, + BLColorRGBA, + BLEnumElement, + BLEnumID, + BLEventType, + BLEventValue, + BLIcon, + BLIconSet, + BLIDStruct, + BLImportMethod, + BLModifierType, + BLNodeTreeInterfaceID, + BLOperatorStatus, + BLPropFlag, + BLRegionType, + BLSpaceType, + PresetName, + PropName, + SocketName, +) +from .bl_handlers import BLHandlers +from .bl_keymap import BLKeymapItem +from .icons import Icon +from .operator_types import ( + OperatorType, +) +from .panel_types import ( + PanelType, +) + +__all__ = [ + 'addon', + 'BLClass', + 'BLColorRGBA', + 'BLEnumElement', + 'BLEnumID', + 'BLEventType', + 'BLEventValue', + 'BLIcon', + 'BLIconSet', + 'BLIDStruct', + 'BLImportMethod', + 'BLKeymapItem', + 'BLModifierType', + 'BLNodeTreeInterfaceID', + 'BLOperatorStatus', + 'BLPropFlag', + 'BLRegionType', + 'BLSpaceType', + 'KeymapItemDef', + 'ManagedObjName', + 'PresetName', + 'PropName', + 'SocketName', + 'BLHandlers', + 'Icon', + 'BLInstance', + 'InstanceID', + 'NodeTreeType', + 'OperatorType', + 'PanelType', +] diff --git a/examples/simple/blext_simple_example/contracts/addon.py b/examples/simple/blext_simple_example/contracts/addon.py new file mode 100644 index 0000000..dbcb888 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/addon.py @@ -0,0 +1,49 @@ +"""Addon information deduced from the manifest and `bpy`.""" + +import tomllib +from pathlib import Path + +import bpy + +from .. import __package__ as base_package + +PATH_ADDON_ROOT: Path = Path(__file__).resolve().parent.parent +PATH_MANIFEST: Path = PATH_ADDON_ROOT / 'blender_manifest.toml' + +with PATH_MANIFEST.open('rb') as f: + MANIFEST = tomllib.load(f) + +NAME: str = MANIFEST['id'] +VERSION: str = MANIFEST['version'] + + +#################### +# - Dynamic Access +#################### +def prefs() -> bpy.types.AddonPreferences | None: + """Retrieve the preferences of this addon, if they are available yet. + + Notes: + While registering the addon, one may wish to use the addon preferences. + This isn't possible - not even for default values. + + Either a bad idea is at work, or `oscillode.utils.init_settings` should be consulted until the preferences are available. + + Returns: + The addon preferences, if the addon is registered and loaded - otherwise None. + """ + addon = bpy.context.preferences.addons.get(base_package) + if addon is None: + return None + return addon.preferences + + +def addon_dir() -> Path: + """Absolute path to a local addon-specific directory guaranteed to be writable.""" + return Path( + bpy.utils.extension_path_user( + base_package, + path='', + create=True, + ) + ).resolve() diff --git a/examples/simple/blext_simple_example/contracts/bl.py b/examples/simple/blext_simple_example/contracts/bl.py new file mode 100644 index 0000000..652b11d --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/bl.py @@ -0,0 +1,283 @@ +"""Explicit type annotations for Blender objects, making it easier to guarantee correctness in communications with Blender.""" + +import typing as typ + +import bpy + +#################### +# - Blender Strings +#################### +BLEnumID: typ.TypeAlias = str +SocketName: typ.TypeAlias = str +PropName: typ.TypeAlias = str + +#################### +# - Blender Enums +#################### +BLImportMethod: typ.TypeAlias = typ.Literal['append', 'link'] +BLModifierType: typ.TypeAlias = typ.Literal['NODES', 'ARRAY'] +BLNodeTreeInterfaceID: typ.TypeAlias = str + +BLIcon: typ.TypeAlias = str +BLIconSet: frozenset[BLIcon] = frozenset( + bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon'].enum_items.keys() +) + +BLEnumElement = tuple[BLEnumID, str, str, BLIcon, int] + +#################### +# - Blender Structs +#################### +BLClass: typ.TypeAlias = ( + bpy.types.Panel + | bpy.types.UIList + | bpy.types.Menu + | bpy.types.Header + | bpy.types.Operator + | bpy.types.KeyingSetInfo + | bpy.types.RenderEngine + | bpy.types.AssetShelf + | bpy.types.FileHandler +) +BLIDStruct: typ.TypeAlias = ( + bpy.types.Action + | bpy.types.Armature + | bpy.types.Brush + | bpy.types.CacheFile + | bpy.types.Camera + | bpy.types.Collection + | bpy.types.Curve + | bpy.types.Curves + | bpy.types.FreestyleLineStyle + | bpy.types.GreasePencil + | bpy.types.Image + | bpy.types.Key + | bpy.types.Lattice + | bpy.types.Library + | bpy.types.Light + | bpy.types.LightProbe + | bpy.types.Mask + | bpy.types.Material + | bpy.types.Mesh + | bpy.types.MetaBall + | bpy.types.MovieClip + | bpy.types.NodeTree + | bpy.types.Object + | bpy.types.PaintCurve + | bpy.types.Palette + | bpy.types.ParticleSettings + | bpy.types.PointCloud + | bpy.types.Scene + | bpy.types.Screen + | bpy.types.Sound + | bpy.types.Speaker + | bpy.types.Text + | bpy.types.Texture + | bpy.types.VectorFont + | bpy.types.Volume + | bpy.types.WindowManager + | bpy.types.WorkSpace + | bpy.types.World +) +BLPropFlag: typ.TypeAlias = typ.Literal[ + 'HIDDEN', + 'SKIP_SAVE', + 'SKIP_PRESET', + 'ANIMATABLE', + 'LIBRARY_EDITABLE', + 'PROPORTIONAL', + 'TEXTEDIT_UPDATE', + 'OUTPUT_PATH', +] +BLColorRGBA = tuple[float, float, float, float] + + +#################### +# - Operators +#################### +BLRegionType: typ.TypeAlias = typ.Literal[ + 'WINDOW', + 'HEADER', + 'CHANNELS', + 'TEMPORARY', + 'UI', + 'TOOLS', + 'TOOL_PROPS', + 'ASSET_SHELF', + 'ASSET_SHELF_HEADER', + 'PREVIEW', + 'HUD', + 'NAVIGATION_BAR', + 'EXECUTE', + 'FOOTER', + 'TOOL_HEADER', + 'XR', +] +BLSpaceType: typ.TypeAlias = typ.Literal[ + 'EMPTY', + 'VIEW_3D', + 'IMAGE_EDITOR', + 'NODE_EDITOR', + 'SEQUENCE_EDITOR', + 'CLIP_EDITOR', + 'DOPESHEET_EDITOR', + 'GRAPH_EDITOR', + 'NLA_EDITOR', + 'TEXT_EDITOR', + 'CONSOLE', + 'INFO', + 'TOPBAR', + 'STATUSBAR', + 'OUTLINER', + 'PROPERTIES', + 'FILE_BROWSER', + 'SPREADSHEET', + 'PREFERENCES', +] +BLOperatorStatus: typ.TypeAlias = set[ + typ.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE'] +] + +#################### +# - Operators +#################### +## TODO: Write the rest in. +BLEventType: typ.TypeAlias = typ.Literal[ + 'NONE', + 'LEFTMOUSE', + 'MIDDLEMOUSE', + 'RIGHTMOUSE', + 'BUTTON4MOUSE', + 'BUTTON5MOUSE', + 'BUTTON6MOUSE', + 'BUTTON7MOUSE', + 'PEN', + 'ERASOR', + 'MOUSEMOVE', + 'INBETWEEN_MOUSEMOVE', + 'TRACKPADPAN', + 'TRACKPADZOOM', + 'MOUSEROTATE', + 'MOUSESMARTZOOM', + 'WHEELUPMOUSE', + 'WHEELDOWNMOUSE', + 'WHEELINMOUSE', + 'WHEELOUTMOUSE', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'ZERO', + 'ONE', + 'TWO', + 'THREE', + 'FOUR', + 'FIVE', + 'SIX', + 'SEVEN', + 'EIGHT', + 'NINE', + 'LEFT_CTRL', + 'LEFT_ALT', + 'LEFT_SHIFT', + 'RIGHT_ALT', + 'RIGHT_CTRL', + 'RIGHT_SHIFT', + 'ESC', + 'TAB', + 'RET', ## Enter + 'SPACE', + 'LINE_FEED', + 'BACK_SPACE', + 'DEL', + 'SEMI_COLON', + 'PERIOD', + 'COMMA', + 'QUOTE', + 'ACCENT_GRAVE', + 'MINUS', + 'PLUS', + 'SLASH', + 'BACK_SLASH', + 'EQUAL', + 'LEFT_BRACKET', + 'RIGHT_BRACKET', + 'LEFT_ARROW', + 'DOWN_ARROW', + 'RIGHT_ARROW', + 'UP_ARROW', + 'NUMPAD_0', + 'NUMPAD_1', + 'NUMPAD_2', + 'NUMPAD_3', + 'NUMPAD_4', + 'NUMPAD_5', + 'NUMPAD_6', + 'NUMPAD_7', + 'NUMPAD_8', + 'NUMPAD_9', + 'NUMPAD_PERIOD', + 'NUMPAD_SLASH', + 'NUMPAD_ASTERIX', + 'NUMPAD_MINUS', + 'NUMPAD_PLUS', + 'NUMPAD_ENTER', + 'F1', + 'F2', + 'F3', + 'F4', + 'F5', + 'F6', + 'F7', + 'F8', + 'F9', + 'F10', + 'F11', + 'F12', + 'PAUSE', + 'INSERT', + 'HOME', + 'PAGE_UP', + 'PAGE_DOWN', + 'END', + 'MEDIA_PLAY', + 'MEDIA_STOP', + 'MEDIA_FIRST', + 'MEDIA_LAST', +] +BLEventValue: typ.TypeAlias = typ.Literal[ + 'ANY', + 'PRESS', + 'RELEASE', + 'CLICK', + 'DOUBLE_CLICK', + 'CLICK_DRAG', + 'NOTHING', +] + +#################### +# - Blender Strings +#################### +PresetName = str diff --git a/examples/simple/blext_simple_example/contracts/bl_handlers.py b/examples/simple/blext_simple_example/contracts/bl_handlers.py new file mode 100644 index 0000000..198ab46 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/bl_handlers.py @@ -0,0 +1,115 @@ +"""Declares a structure for aggregating `bpy.app.handlers` callbacks.""" + +import typing as typ + +import bpy +import pydantic as pyd + +from ..utils.staticproperty import staticproperty + +BLHandler = typ.Callable[[], None] +BLHandlerWithFile = typ.Callable[[str], None] +BLHandlerWithRenderStats = typ.Callable[[typ.Any], None] + + +class BLHandlers(pyd.BaseModel): + """Contains lists of handlers associated with this addon.""" + + animation_playback_post: tuple[BLHandler, ...] = () + animation_playback_pre: tuple[BLHandler, ...] = () + annotation_post: tuple[BLHandler, ...] = () + annotation_pre: tuple[BLHandler, ...] = () + composite_cancel: tuple[BLHandler, ...] = () + composite_post: tuple[BLHandler, ...] = () + composite_pre: tuple[BLHandler, ...] = () + depsgraph_update_post: tuple[BLHandler, ...] = () + depsgraph_update_pre: tuple[BLHandler, ...] = () + frame_change_post: tuple[BLHandler, ...] = () + frame_change_pre: tuple[BLHandler, ...] = () + load_factory_preferences_post: tuple[BLHandler, ...] = () + load_factory_startup_post: tuple[BLHandler, ...] = () + load_post: tuple[BLHandlerWithFile, ...] = () + load_post_fail: tuple[BLHandlerWithFile, ...] = () + load_pre: tuple[BLHandlerWithFile, ...] = () + object_bake_cancel: tuple[BLHandler, ...] = () + object_bake_complete: tuple[BLHandler, ...] = () + object_bake_pre: tuple[BLHandler, ...] = () + redo_post: tuple[BLHandler, ...] = () + redo_pre: tuple[BLHandler, ...] = () + render_cancel: tuple[BLHandler, ...] = () + render_complete: tuple[BLHandler, ...] = () + render_init: tuple[BLHandler, ...] = () + render_post: tuple[BLHandler, ...] = () + render_pre: tuple[BLHandler, ...] = () + render_stats: tuple[BLHandler, ...] = () + render_write: tuple[BLHandler, ...] = () + save_post: tuple[BLHandlerWithFile, ...] = () + save_post_fail: tuple[BLHandlerWithFile, ...] = () + save_pre: tuple[BLHandlerWithFile, ...] = () + version_update: tuple[BLHandler, ...] = () + xr_session_start_pre: tuple[BLHandler, ...] = () + ## TODO: Verify these type signatures. + + ## TODO: A validator to check that all handlers are decorated with bpy.app.handlers.persistent + + #################### + # - Properties + #################### + @staticproperty # type: ignore[arg-type] + def handler_categories() -> tuple[str, ...]: # type: ignore[misc] + """Returns an immutable string sequence of handler categories.""" + return ( + 'animation_playback_post', + 'animation_playback_pre', + 'annotation_post', + 'annotation_pre', + 'composite_cancel', + 'composite_post', + 'composite_pre', + 'depsgraph_update_post', + 'depsgraph_update_pre', + 'frame_change_post', + 'frame_change_pre', + 'load_factory_preferences_post', + 'load_factory_startup_post', + 'load_post', + 'load_post_fail', + 'load_pre', + 'object_bake_cancel', + 'object_bake_complete', + 'object_bake_pre', + 'redo_post', + 'redo_pre', + 'render_cancel', + 'render_complete', + 'render_init', + 'render_post', + 'render_pre', + 'render_stats', + ) + + #################### + # - Merging + #################### + def __add__(self, other: typ.Self) -> typ.Self: + """Concatenate two sets of handlers.""" + return self.__class__( + **{ + hndl_cat: getattr(self, hndl_cat) + getattr(self, hndl_cat) + for hndl_cat in self.handler_categories + } + ) + + def register(self) -> None: + """Registers all handlers, by-category.""" + for handler_category in BLHandlers.handler_categories: + for handler in getattr(self, handler_category): + getattr(bpy.app.handlers, handler_category).append(handler) + + def unregister(self) -> None: + """Unregisters only this object's handlers from bpy.app.handlers.""" + for handler_category in BLHandlers.handler_categories: + for handler in getattr(self, handler_category): + bpy_handlers = getattr(bpy.app.handlers, handler_category) + if handler in bpy_handlers: + bpy_handlers.remove(handler) diff --git a/examples/simple/blext_simple_example/contracts/bl_keymap.py b/examples/simple/blext_simple_example/contracts/bl_keymap.py new file mode 100644 index 0000000..b5dc471 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/bl_keymap.py @@ -0,0 +1,44 @@ +"""Declares types for working with `bpy.types.KeyMap`s.""" + +import bpy +import pydantic as pyd + +from .bl import BLEventType, BLEventValue, BLSpaceType +from .operator_types import OperatorType + + +class BLKeymapItem(pyd.BaseModel): + """Contains lists of handlers associated with this addon.""" + + operator: OperatorType + + event_type: BLEventType + event_value: BLEventValue + + shift: bool = False + ctrl: bool = False + alt: bool = False + key_modifier: BLEventType = 'NONE' + + space_type: BLSpaceType = 'EMPTY' + + def register(self, addon_keymap: bpy.types.KeyMap) -> bpy.types.KeyMapItem: + """Registers this hotkey with an addon keymap. + + Raises: + ValueError: If the `space_type` constraint of the addon keymap does not match the `space_type` constraint of this `BLKeymapItem`. + """ + if self.space_type == addon_keymap.space_type: + addon_keymap.keymap_items.new( + self.operator, + self.event_type, + self.event_value, + shift=self.shift, + ctrl=self.ctrl, + alt=self.alt, + key_modifier=self.key_modifier, + ) + + msg = f'Addon keymap space type {addon_keymap.space_type} does not match space_type of BLKeymapItem to register: {self}' + raise ValueError(msg) + ## TODO: Check if space_type matches? diff --git a/examples/simple/blext_simple_example/contracts/icons.py b/examples/simple/blext_simple_example/contracts/icons.py new file mode 100644 index 0000000..e431211 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/icons.py @@ -0,0 +1,7 @@ +"""Provides an enum that semantically constrains the use of icons throughout the addon.""" + +import enum + + +class Icon(enum.StrEnum): + """Identifiers for icons used throughout this addon.""" diff --git a/examples/simple/blext_simple_example/contracts/operator_types.py b/examples/simple/blext_simple_example/contracts/operator_types.py new file mode 100644 index 0000000..4fde2fe --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/operator_types.py @@ -0,0 +1,11 @@ +"""Provides identifiers for Blender operators defined by this addon.""" + +import enum + +from .addon import NAME as ADDON_NAME + + +class OperatorType(enum.StrEnum): + """Identifiers for addon-defined `bpy.types.Operator`.""" + + SimpleOperator = f'{ADDON_NAME}.simple_operator' diff --git a/examples/simple/blext_simple_example/contracts/panel_types.py b/examples/simple/blext_simple_example/contracts/panel_types.py new file mode 100644 index 0000000..05056d3 --- /dev/null +++ b/examples/simple/blext_simple_example/contracts/panel_types.py @@ -0,0 +1,13 @@ +"""Provides identifiers for Blender panels defined by this addon.""" + +import enum + +from .addon import NAME as ADDON_NAME + +PREFIX = f'{ADDON_NAME.upper()}_PT_' + + +class PanelType(enum.StrEnum): + """Identifiers for addon-defined `bpy.types.Panel`.""" + + SimplePanel = f'{PREFIX}simple_panel' diff --git a/examples/simple/blext_simple_example/operators/__init__.py b/examples/simple/blext_simple_example/operators/__init__.py new file mode 100644 index 0000000..0771954 --- /dev/null +++ b/examples/simple/blext_simple_example/operators/__init__.py @@ -0,0 +1,20 @@ +"""Blender operators that ship with `bpy_jupyter`.""" + +from functools import reduce + +from .. import contracts as ct +from . import simple_operator + +BL_REGISTER: list[ct.BLClass] = [ + *simple_operator.BL_REGISTER, +] +BL_HANDLERS: ct.BLHandlers = reduce( + lambda a, b: a + b, + [ + simple_operator.BL_HANDLERS, + ], + ct.BLHandlers(), +) +BL_KEYMAP_ITEMS: list[ct.BLKeymapItem] = [ + *simple_operator.BL_KEYMAP_ITEMS, +] diff --git a/examples/simple/blext_simple_example/operators/simple_operator.py b/examples/simple/blext_simple_example/operators/simple_operator.py new file mode 100644 index 0000000..b7bf3eb --- /dev/null +++ b/examples/simple/blext_simple_example/operators/simple_operator.py @@ -0,0 +1,34 @@ +"""Defines the `StartJupyterKernel` operator. + +Inspired by +""" + +import bpy + +from .. import contracts as ct + + +class SimpleOperator(bpy.types.Operator): + """Operator that shows a message.""" + + bl_idname = ct.OperatorType.SimpleOperator + bl_label = 'Other Operator' + + @classmethod + def poll(cls, _: bpy.types.Context) -> bool: + """Always allow operator to run.""" + return True + + def execute(self, _: bpy.types.Context) -> set[ct.BLOperatorStatus]: + """Display a simple message on execution.""" + self.report({'INFO'}, 'SimpleOperator was executed from blext_simple_example.') + + return {'FINISHED'} + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [SimpleOperator] +BL_HANDLERS: ct.BLHandlers = ct.BLHandlers() +BL_KEYMAP_ITEMS: list[ct.BLKeymapItem] = [] diff --git a/examples/simple/blext_simple_example/panels/__init__.py b/examples/simple/blext_simple_example/panels/__init__.py new file mode 100644 index 0000000..c3eb627 --- /dev/null +++ b/examples/simple/blext_simple_example/panels/__init__.py @@ -0,0 +1,20 @@ +"""Blender panels that ship with `bpy_jupyter`.""" + +from functools import reduce + +from .. import contracts as ct +from . import simple_panel + +BL_REGISTER: list[ct.BLClass] = [ + *simple_panel.BL_REGISTER, +] +BL_HANDLERS: ct.BLHandlers = reduce( + lambda a, b: a + b, + [ + simple_panel.BL_HANDLERS, + ], + ct.BLHandlers(), +) +BL_KEYMAP_ITEMS: list[ct.BLKeymapItem] = [ + *simple_panel.BL_KEYMAP_ITEMS, +] diff --git a/examples/simple/blext_simple_example/panels/simple_panel.py b/examples/simple/blext_simple_example/panels/simple_panel.py new file mode 100644 index 0000000..5592691 --- /dev/null +++ b/examples/simple/blext_simple_example/panels/simple_panel.py @@ -0,0 +1,52 @@ +import bpy + +from .. import contracts as ct + +bpy.types.Scene.simple_integer = bpy.props.IntProperty( + name='Simple Integer', + description="It's just an integer! What's the big deal?", + default=10, +) + + +class SimplePanel(bpy.types.Panel): + bl_idname = ct.PanelType.SimplePanel + bl_label = 'Simple Panel' + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'scene' + + @classmethod + def poll(cls, _: bpy.types.Context) -> bool: + """Always show panel in Scene properties. + + Notes: + Run by Blender when trying to show a panel. + + Returns: + Whether the panel can show. + """ + return True + + def draw(self, _: bpy.types.Context) -> None: + """Draw the panel w/options. + + Notes: + Run by Blender when the panel needs to be displayed. + + Parameters: + context: The Blender context object. + Must contain `context.window_manager` and `context.workspace`. + """ + layout = self.layout + + # Operator + layout.operator(ct.OperatorType.SimpleOperator) + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [SimplePanel] +BL_HANDLERS: ct.BLHandlers = ct.BLHandlers() +BL_KEYMAP_ITEMS: list[ct.BLKeymapItem] = [] diff --git a/examples/simple/blext_simple_example/preferences.py b/examples/simple/blext_simple_example/preferences.py new file mode 100644 index 0000000..7b3bb40 --- /dev/null +++ b/examples/simple/blext_simple_example/preferences.py @@ -0,0 +1,185 @@ +"""Addon preferences, encapsulating the various global modifications that the user may make to how this addon functions.""" + +import logging +from pathlib import Path + +import bpy + +from .services.init_settings import INIT_SETTINGS +from .utils import logger + +log = logger.get(__name__) + + +#################### +# - Constants +#################### +LOG_LEVEL_MAP: dict[str, logger.LogLevel] = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, +} + + +#################### +# - Preferences +#################### +class SimpleAddonPrefs(bpy.types.AddonPreferences): + """Manages user preferences and settings.""" + + bl_idname = __package__ + + #################### + # - Properties + #################### + # Logging + ## File Logging + use_log_file: bpy.props.BoolProperty( # type: ignore + name='Log to File', + description='Whether to use a file for addon logging', + default=INIT_SETTINGS.use_log_file, + update=lambda self, _: self.on_addon_logging_changed(), + ) + log_level_file: bpy.props.EnumProperty( # type: ignore + name='File Log Level', + description='Level of addon logging to expose in the file', + items=[ + ('DEBUG', 'Debug', 'Debug'), + ('INFO', 'Info', 'Info'), + ('WARNING', 'Warning', 'Warning'), + ('ERROR', 'Error', 'Error'), + ('CRITICAL', 'Critical', 'Critical'), + ], + default=INIT_SETTINGS.log_file_level, + update=lambda self, _: self.on_addon_logging_changed(), + ) + + bl__log_file_path: bpy.props.StringProperty( # type: ignore + name='Log Path', + description='Path to the Addon Log File', + subtype='FILE_PATH', + default=str(INIT_SETTINGS.log_file_path), + update=lambda self, _: self.on_addon_logging_changed(), + ) + + @property + def log_file_path(self) -> Path: + """Retrieve the configured file-logging path as a `pathlib.Path`.""" + return Path(bpy.path.abspath(self.bl__log_file_path)) + + @log_file_path.setter + def log_file_path(self, path: Path) -> None: + """Set the configured file-logging path as a `pathlib.Path`.""" + self.bl__log_file_path = str(path.resolve()) + + ## Console Logging + use_log_console: bpy.props.BoolProperty( # type: ignore + name='Log to Console', + description='Whether to use the console for addon logging', + default=INIT_SETTINGS.use_log_console, + update=lambda self, _: self.on_addon_logging_changed(), + ) + log_level_console: bpy.props.EnumProperty( # type: ignore + name='Console Log Level', + description='Level of addon logging to expose in the console', + items=[ + ('DEBUG', 'Debug', 'Debug'), + ('INFO', 'Info', 'Info'), + ('WARNING', 'Warning', 'Warning'), + ('ERROR', 'Error', 'Error'), + ('CRITICAL', 'Critical', 'Critical'), + ], + default=INIT_SETTINGS.log_console_level, + update=lambda self, _: self.on_addon_logging_changed(), + ) + + #################### + # - Events: Properties Changed + #################### + def setup_logger(self, _logger: logging.Logger) -> None: + """Setup a logger using the settings declared in the addon preferences. + + Args: + _logger: The logger to configure using settings in the addon preferences. + """ + logger.update_logger( + logger.console_handler, + logger.file_handler, + _logger, + file_path=self.log_file_path if self.use_log_file else None, + file_level=LOG_LEVEL_MAP[self.log_level_file], + console_level=LOG_LEVEL_MAP[self.log_level_console] + if self.use_log_console + else None, + ) + + def on_addon_logging_changed(self) -> None: + """Called to reconfigure all loggers to match newly-altered addon preferences. + + This causes ex. changes to desired console log level to immediately be applied, but only the this addon's loggers. + + Parameters: + single_logger_to_setup: When set, only this logger will be setup. + Otherwise, **all addon loggers will be setup**. + """ + log.info('Reconfiguring Loggers') + for _logger in logger.all_addon_loggers(): + self.setup_logger(_logger) + + log.info('Loggers Reconfigured') + + #################### + # - UI + #################### + def draw(self, _: bpy.types.Context) -> None: + """Draw the addon preferences within its panel in Blender's preferences. + + Notes: + Run by Blender when this addon's preferences need to be displayed. + + Parameters: + context: The Blender context object. + """ + layout = self.layout + + #################### + # - Logging + #################### + # Box w/Split: Log Level + box = layout.box() + row = box.row() + row.alignment = 'CENTER' + row.label(text='Logging') + split = box.split(factor=0.5) + + ## Split Col: Console Logging + col = split.column() + row = col.row() + row.prop(self, 'use_log_console', toggle=True) + + row = col.row() + row.enabled = self.use_log_console + row.prop(self, 'log_level_console') + + ## Split Col: File Logging + col = split.column() + row = col.row() + row.prop(self, 'use_log_file', toggle=True) + + row = col.row() + row.enabled = self.use_log_file + row.prop(self, 'bl__log_file_path') + + row = col.row() + row.enabled = self.use_log_file + row.prop(self, 'log_level_file') + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + SimpleAddonPrefs, +] diff --git a/examples/simple/blext_simple_example/registration.py b/examples/simple/blext_simple_example/registration.py new file mode 100644 index 0000000..f33ecda --- /dev/null +++ b/examples/simple/blext_simple_example/registration.py @@ -0,0 +1,143 @@ +"""Manages the registration of Blender classes, including delayed registrations that require access to Python dependencies. + +Attributes: + _REGISTERED_CLASSES: Blender classes currently registered by this addon. + _REGISTERED_KEYMAPS: Addon keymaps currently registered by this addon. + Each addon keymap is constrained to a single `space_type`, which is the key. + _REGISTERED_KEYMAP_ITEMS: Addon keymap items currently registered by this addon. + Each keymap item is paired to the keymap within which it is registered. + _Each keymap is guaranteed to also be found in `_REGISTERED_KEYMAPS`._ + _REGISTERED_HANDLERS: Addon handlers currently registered by this addon. +""" + +import bpy + +from . import contracts as ct +from .utils import logger + +log = logger.get(__name__) + +#################### +# - Globals +#################### +_REGISTERED_CLASSES: list[ct.BLClass] = [] + +_REGISTERED_KEYMAPS: dict[ct.BLSpaceType, bpy.types.KeyMap] = {} +_REGISTERED_KEYMAP_ITEMS: list[tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = [] + +_REGISTERED_HANDLERS: ct.BLHandlers | None = None + + +#################### +# - Class Registration +#################### +def register_classes(bl_register: list[ct.BLClass]) -> None: + """Registers a list of Blender classes. + + Parameters: + bl_register: List of Blender classes to register. + """ + log.info('Registering %s Classes', len(bl_register)) + for cls in bl_register: + if cls.bl_idname in _REGISTERED_CLASSES: + msg = f'Skipping register of {cls.bl_idname}' + log.info(msg) + continue + + log.debug( + 'Registering Class %s', + repr(cls), + ) + bpy.utils.register_class(cls) + _REGISTERED_CLASSES.append(cls) + + +def unregister_classes() -> None: + """Unregisters all previously registered Blender classes.""" + log.info('Unregistering %s Classes', len(_REGISTERED_CLASSES)) + for cls in reversed(_REGISTERED_CLASSES): + log.debug( + 'Unregistering Class %s', + repr(cls), + ) + bpy.utils.unregister_class(cls) + + _REGISTERED_CLASSES.clear() + + +#################### +# - Handler Registration +#################### +def register_handlers(bl_handlers: ct.BLHandlers) -> None: + """Register the given Blender handlers.""" + global _REGISTERED_HANDLERS # noqa: PLW0603 + + log.info('Registering BLHandlers') ## TODO: More information + if _REGISTERED_HANDLERS is None: + bl_handlers.register() + _REGISTERED_HANDLERS = bl_handlers + else: + msg = 'There are already BLHandlers registered; they must be unregistered before a new set can be registered.' + raise ValueError(msg) + + +def unregister_handlers() -> None: + """Unregister this addon's registered Blender handlers.""" + global _REGISTERED_HANDLERS # noqa: PLW0603 + + log.info('Unregistering BLHandlers') ## TODO: More information + if _REGISTERED_HANDLERS is not None: + _REGISTERED_HANDLERS.register() + _REGISTERED_HANDLERS = None + else: + msg = 'There are no BLHandlers registered; therefore, there is nothing to register.' + raise ValueError(msg) + + +#################### +# - Keymap Registration +#################### +def register_keymaps(keymap_items: list[ct.BLKeymapItem]) -> None: + """Registers a list of Blender hotkey definitions. + + Parameters: + bl_keymap_items: List of Blender hotkey definitions to register. + """ + # Aggregate Requested Spaces of All Keymap Items + keymap_space_types: set[ct.BLSpaceType] = { + keymap_item.space_type for keymap_item in keymap_items + } + + # Create Addon Keymap per Requested Space + for keymap_space_type in keymap_space_types: + log.info('Registering %s Keymap', keymap_space_type) + bl_keymap = bpy.context.window_manager.keyconfigs.addon.keymaps.new( + name=f'{ct.addon.NAME} - {keymap_space_type}', + space_type=keymap_space_type, + ) + _REGISTERED_KEYMAPS[keymap_space_type] = bl_keymap + + # Register Keymap Items in Correct Addon Keymap + for keymap_item in keymap_items: + log.info('Registering %s Keymap Item', keymap_item) + bl_keymap = _REGISTERED_KEYMAPS[keymap_item.space_type] + bl_keymap_item = keymap_item.register(bl_keymap) + + _REGISTERED_KEYMAP_ITEMS.append((bl_keymap, bl_keymap_item)) + + +def unregister_keymaps() -> None: + """Unregisters all Blender keymaps associated with the addon.""" + # Unregister Keymap Items from Correct Addon Keymap + for bl_keymap, bl_keymap_item in reversed(_REGISTERED_KEYMAP_ITEMS): + log.info('Unregistering %s BL Keymap Item', bl_keymap_item) + bl_keymap.keymap_items.remove(bl_keymap_item) + + _REGISTERED_KEYMAP_ITEMS.clear() + + # Delete Addon Keymaps + for bl_keymap in reversed(_REGISTERED_KEYMAPS.values()): + log.info('Unregistering %s Keymap', bl_keymap) + bpy.context.window_manager.keyconfigs.addon.keymaps.remove(bl_keymap) + + _REGISTERED_KEYMAPS.clear() diff --git a/examples/simple/blext_simple_example/services/__init__.py b/examples/simple/blext_simple_example/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/simple/blext_simple_example/services/init_settings.py b/examples/simple/blext_simple_example/services/init_settings.py new file mode 100644 index 0000000..f406f89 --- /dev/null +++ b/examples/simple/blext_simple_example/services/init_settings.py @@ -0,0 +1,58 @@ +"""Authoritative definition and storage of "Initialization Settings", which provide out-of-the-box defaults for settings relevant for ex. debugging. + +These settings are transported via a special TOML file that is packed into the extension zip-file when packaging for release. +""" + +import logging +import tomllib +import typing as typ +from pathlib import Path + +import pydantic as pyd + +#################### +# - Constants +#################### +PATH_ROOT = Path(__file__).resolve().parent.parent +INIT_SETTINGS_FILENAME = 'init_settings.toml' + + +#################### +# - Types +#################### +LogLevel: typ.TypeAlias = int +StrLogLevel: typ.TypeAlias = typ.Literal[ + 'DEBUG', + 'INFO', + 'WARNING', + 'ERROR', + 'CRITICAL', +] + + +def str_to_loglevel(obj: typ.Any) -> LogLevel | typ.Any: + if isinstance(obj, str) and obj in STR_LOG_LEVEL: + return STR_LOG_LEVEL[obj] + return obj + + +class InitSettings(pyd.BaseModel): + """Model describing addon initialization settings, describing default settings baked into the release. + + Each parameter + + """ + + use_log_file: bool + log_file_path: Path + log_file_level: StrLogLevel + + use_log_console: bool + log_console_level: StrLogLevel + + +#################### +# - Init Settings Management +#################### +with (PATH_ROOT / INIT_SETTINGS_FILENAME).open('rb') as f: + INIT_SETTINGS: InitSettings = InitSettings(**tomllib.load(f)) diff --git a/examples/simple/blext_simple_example/utils/__init__.py b/examples/simple/blext_simple_example/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/simple/blext_simple_example/utils/logger.py b/examples/simple/blext_simple_example/utils/logger.py new file mode 100644 index 0000000..a261466 --- /dev/null +++ b/examples/simple/blext_simple_example/utils/logger.py @@ -0,0 +1,196 @@ +"""A lightweight, `rich`-based approach to logging that is isolated from other extensions that may used the Python stdlib `logging` module.""" + +import logging +import typing as typ +from pathlib import Path + +import rich.console +import rich.logging +import rich.traceback + +from .. import contracts as ct +from ..services import init_settings + +LogLevel: typ.TypeAlias = int + + +#################### +# - Configuration +#################### +STREAM_LOG_FORMAT = 11 * ' ' + '%(levelname)-8s %(message)s (%(name)s)' +FILE_LOG_FORMAT = STREAM_LOG_FORMAT + +OUTPUT_CONSOLE = rich.console.Console( + color_system='truecolor', +) +ERROR_CONSOLE = rich.console.Console( + color_system='truecolor', + stderr=True, +) + +ADDON_LOGGER_NAME = f'blext-{ct.addon.NAME}' +ADDON_LOGGER: logging.Logger = logging.getLogger(ADDON_LOGGER_NAME) + +rich.traceback.install(show_locals=True, console=ERROR_CONSOLE) + + +#################### +# - Logger Access +#################### +def all_addon_loggers() -> set[logging.Logger]: + """Retrieve all loggers currently declared by this addon. + + These loggers are all children of `ADDON_LOGGER`, essentially. + This allows for name-isolation from other Blender extensions, as well as easy cleanup. + + Returns: + Set of all loggers declared by this addon. + """ + return { + logging.getLogger(name) + for name in logging.root.manager.loggerDict + if name.startswith(ADDON_LOGGER_NAME) + } + ## TODO: Python 3.12 has a .getChildren() method that also returns sets. + + +#################### +# - Logging Handlers +#################### +def console_handler(level: LogLevel) -> rich.logging.RichHandler: + """A logging handler that prints messages to the console. + + Parameters: + level: The log levels (debug, info, etc.) to print. + + Returns: + The logging handler, which can be added to a logger. + """ + rich_formatter = logging.Formatter( + '%(message)s', + datefmt='[%X]', + ) + rich_handler = rich.logging.RichHandler( + level=level, + console=ERROR_CONSOLE, + rich_tracebacks=True, + ) + rich_handler.setFormatter(rich_formatter) + return rich_handler + + +def file_handler(path_log_file: Path, level: LogLevel) -> rich.logging.RichHandler: + """A logging handler that prints messages to a file. + + Parameters: + path_log_file: The path to the log file. + level: The log levels (debug, info, etc.) to append to the file. + + Returns: + The logging handler, which can be added to a logger. + """ + file_formatter = logging.Formatter(FILE_LOG_FORMAT) + file_handler = logging.FileHandler(path_log_file) + file_handler.setFormatter(file_formatter) + file_handler.setLevel(level) + return file_handler + + +#################### +# - Logger Setup +#################### +def get(module_name: str) -> logging.Logger: + """Retrieve and/or create a logger corresponding to a module name. + + Warnings: + MUST be used as `logger.get(__name__)`. + + Parameters: + module_name: The `__name__` of the module to return a logger for. + """ + log = ADDON_LOGGER.getChild(module_name) + + # Setup Logger from Init Settings or Addon Preferences + ## - We prefer addon preferences, but they may not be setup yet. + ## - Once setup, the preferences may decide to re-configure all the loggers. + addon_prefs = ct.addon.prefs() + if addon_prefs is None: + use_log_file = init_settings.INIT_SETTINGS.use_log_file + log_file_path = init_settings.INIT_SETTINGS.log_file_path + log_file_level = init_settings.INIT_SETTINGS.log_file_level + use_log_console = init_settings.INIT_SETTINGS.use_log_console + log_console_level = init_settings.INIT_SETTINGS.log_console_level + + update_logger( + console_handler, + file_handler, + log, + file_path=log_file_path if use_log_file else None, + file_level=log_file_level, + console_level=log_console_level if use_log_console else None, + ) + else: + addon_prefs.setup_logger(log) + + return log + + +#################### +# - Logger Update +#################### +def _init_logger(logger: logging.Logger) -> None: + """Prepare a logger for handlers to be added, ensuring normalized semantics for all loggers. + + - Messages should not propagate to the root logger, causing double-messages. + - Mesages should not filter by level; this is the job of the handlers. + - No handlers must be set. + + Args: + logger: The logger to prepare. + """ + # DO NOT Propagate to Root Logger + ## - This looks like 'double messages' + ## - See SO/6729268/log-messages-appearing-twice-with-python-logging + logger.propagate = False + + # Let All Messages Through + ## - The individual handlers perform appropriate filtering. + logger.setLevel(logging.NOTSET) + + if logger.handlers: + logger.handlers.clear() + + +def update_logger( + cb_console_handler: typ.Callable[[LogLevel], logging.Handler], + cb_file_handler: typ.Callable[[Path, LogLevel], logging.Handler], + logger: logging.Logger, + console_level: LogLevel | None, + file_path: Path | None, + file_level: LogLevel, +) -> None: + """Configures a single logger with given console and file handlers, individualizing the log level that triggers it. + + This is a lower-level function - generally, modules that want to use a well-configured logger will use the `get()` function, which retrieves the parameters for this function from the addon preferences. + This function is used by the higher-level log setup. + + Parameters: + cb_console_handler: A function that takes a log level threshold (inclusive), and returns a logging handler to a console-printer. + cb_file_handler: A function that takes a log level threshold (inclusive), and returns a logging handler to a file-printer. + logger: The logger to configure. + console_level: The log level threshold to print to the console. + None deactivates file logging. + path_log_file: The path to the log file. + None deactivates file logging. + file_level: The log level threshold to print to the log file. + """ + # Initialize Logger + _init_logger(logger) + + # Add Console Logging Handler + if console_level is not None: + logger.addHandler(cb_console_handler(console_level)) + + # Add File Logging Handler + if file_path is not None: + logger.addHandler(cb_file_handler(file_path, file_level)) diff --git a/examples/simple/blext_simple_example/utils/staticproperty.py b/examples/simple/blext_simple_example/utils/staticproperty.py new file mode 100644 index 0000000..2002b77 --- /dev/null +++ b/examples/simple/blext_simple_example/utils/staticproperty.py @@ -0,0 +1,30 @@ +"""Provides a '@staticproperty', which is like '@property', but static. It can be very useful in specific situations.""" + +import typing as typ + + +class staticproperty(property): # noqa: N801 + """A read-only variant of `@property` that is entirely static, for use in specific situations. + + The decorated method must take no arguments whatsoever, including `self`/`cls`. + + Examples: + Exactly as you'd expect. + ```python + class Spam: + @staticproperty + def eggs(): + return 10 + + assert Spam.eggs == 10 + ``` + """ + + def __get__(self, instance: typ.Any, owner: type | None = None) -> typ.Any: + """Overridden getter that ignores instance and owner, and just returns the value of the evaluated (static) method. + + Returns: + The evaluated value of the static method that was decorated. + """ + return self.fget() # type: ignore + ## .fget() is guaranteed to exist by @property, so we can safely ignore the type. diff --git a/examples/simple/pyproject.toml b/examples/simple/pyproject.toml new file mode 100644 index 0000000..46cf9ca --- /dev/null +++ b/examples/simple/pyproject.toml @@ -0,0 +1,200 @@ +[project] +name = "blext_simple_example" +version = "0.1.0" +description = "Simple real-world example of a Blender extension" +authors = [ + { name = "John Doe", email = "john.doe@example.com" }, +] +maintainers = [ + { name = "John Doe", email = "john.doe@example.com" }, +] +readme = "README.md" +requires-python = "~= 3.11" +license = { text = "AGPL-3.0-or-later" } +dependencies = [ + "pydantic>=2.10.5", + "rich>=13.9.3", +] + +#################### +# - Blender Extension +#################### +[tool.blext] +pretty_name = "BLExt Simple Example" +blender_version_min = '4.2.0' +blender_version_max = '4.3.10' +bl_tags = ["Development"] +copyright = ["2024 blext Contributors"] + +# Platform Support +## Map Valid Blender Platforms -> Required PyPi Platform Tags +## Include as few PyPi tags as works on ~everything. +[tool.blext.platforms] +windows-amd64 = ['win_amd64'] +macos-arm64 = ['macosx_11_0_arm64', 'macosx_12_0_arm64', 'macosx_14_0_arm64'] +linux-x64 = ['manylinux1_x86_64', 'manylinux2014_x86_64', 'manylinux_2_17_x86_64', 'manylinux_2_28_x86_64'] + +# Packaging +## Path is from the directory containing this file. +[tool.blext.packaging] +init_settings_filename = 'init_settings.toml' + +# "Profiles" -> Affects Initialization Settings +## This sets the default extension preferences for different situations. +[tool.blext.profiles.test] +use_path_local = true +use_log_file = true +log_file_path = 'addon.log' +log_file_level = 'DEBUG' +use_log_console = true +log_console_level = 'INFO' + +[tool.blext.profiles.dev] +use_path_local = true +use_log_file = true +log_file_path = 'addon.log' +log_file_level = 'DEBUG' +use_log_console = true +log_console_level = 'INFO' + +[tool.blext.profiles.release] +use_path_local = false +use_log_file = true +log_file_path = 'addon.log' +log_file_level = 'INFO' +use_log_console = true +log_console_level = 'WARNING' + +[tool.blext.profiles.release-debug] +use_path_local = false +use_log_file = true +log_file_path = 'addon.log' +log_file_level = 'DEBUG' +use_log_console = true +log_console_level = 'WARNING' + +#################### +# - Blender Extension +#################### +[tool.uv] +managed = true +dev-dependencies = [ + "mypy>=1.13.0", + "pip>=24.2", + "rich>=13.9.3", + "ruff>=0.7.1", + "tomli-w>=1.1.0", + "typer>=0.15.1", +] +package = false + +#################### +# - Tooling: Ruff +#################### +[tool.ruff] +target-version = "py311" +line-length = 88 + +[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! + #"RUF012", # ruff misunderstands which ClassVars are actually mutable. + + # 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 +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "SLF001", # It's okay to not have module-level docstrings in test modules. + "D100", # It's okay to not have module-level docstrings in test modules. + "D104", # Same for packages. +] + +#################### +# - Tooling: Ruff Sublinters +#################### +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 120 +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: Pytest +#################### +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/examples/simple/uv.lock b/examples/simple/uv.lock new file mode 100644 index 0000000..fe18efe --- /dev/null +++ b/examples/simple/uv.lock @@ -0,0 +1,298 @@ +version = 1 +requires-python = ">=3.11, <4" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "blext-simple-example" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, + { name = "rich" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pip" }, + { name = "rich" }, + { name = "ruff" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.10.5" }, + { name = "rich", specifier = ">=13.9.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.13.0" }, + { name = "pip", specifier = ">=24.2" }, + { name = "rich", specifier = ">=13.9.3" }, + { name = "ruff", specifier = ">=0.7.1" }, + { name = "tomli-w", specifier = ">=1.1.0" }, + { name = "typer", specifier = ">=0.15.1" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "pip" +version = "24.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, +] + +[[package]] +name = "pydantic" +version = "2.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "ruff" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241 }, + { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960 }, + { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803 }, + { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929 }, + { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717 }, + { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921 }, + { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074 }, + { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093 }, + { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610 }, + { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273 }, + { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314 }, + { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982 }, + { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750 }, + { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331 }, + { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..595269a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,201 @@ +[project] +name = "blext" +version = "0.1.0" +description = "Fast, convenient project manager for Blender Extensions." +authors = [ + { name = "Sofus Albert Høgsbro Rose", email = "blext@sofusrose.com" }, +] +maintainers = [ + { name = "Sofus Albert Høgsbro Rose", email = "blext@sofusrose.com" }, +] + +readme = "README.md" +requires-python = ">=3.11" +license = { text = "AGPL-3.0-or-later" } +dependencies = [ + "cyclopts>=3.1.5", + "pip>=24.3.1", + "pydantic>=2.10.5", + "pypdl>=1.5.1", + "rich>=13.9.4", + "tomli-w>=1.1.0", +] + +[dependency-groups] +dev = [ + "mypy>=1.14.1", + "pytest>=8.3.4", + "ruff>=0.9.1", +] + +[project.scripts] +blext = "blext.cli:app" + +[tool.setuptools] +packages = ["blext"] + +[tool.uv] +package = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +#################### +# - Tooling: Ruff +#################### +[tool.ruff] +target-version = "py311" +line-length = 88 + +[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 + "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 +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "SLF001", # It's okay to not have module-level docstrings in test modules. + "D100", # It's okay to not have module-level docstrings in test modules. + "D104", # Same for packages. +] + +#################### +# - Tooling: Ruff Sublinters +#################### +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [] + +[tool.ruff.lint.pycodestyle] +max-doc-length = 120 +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: MyPy +#################### +[tool.mypy] +python_version = '3.12' +python_executable="./.venv/bin/python" + +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true + +strict_optional = true +no_implicit_optional = true + +disallow_subclassing_any = false +disallow_any_generics = true +disallow_untyped_calls = true +disallow_incomplete_defs = true + +check_untyped_defs = true +disallow_untyped_decorators = true + +ignore_missing_imports = true + +plugins = [ +# 'pydantic.mypy', +# 'typing_protocol_intersection.mypy_plugin', +] + + +#################### +# - 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 + +#################### +# - Tooling: Pytest +#################### +[tool.pytest.ini_options] +testpaths = ["tests"] + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1c9ed52 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.dont_write_bytecode = True diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..24c6805 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,7 @@ +from pathlib import Path + +PATH_ROOT = Path(__file__).resolve().parent.parent + +PATH_EXAMPLES = { + 'simple': PATH_ROOT / 'examples' / 'simple' +} diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..2dde59f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,15 @@ +import contextlib + +from typer.testing import CliRunner + +from blext.cli import app +from tests import context + +runner = CliRunner() + + +def test_build_simple(): + with contextlib.chdir(context.PATH_EXAMPLES['simple']): + result = runner.invoke(app, ['build']) + print(result) + assert result.exit_code == 0 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cf0bf6c --- /dev/null +++ b/uv.lock @@ -0,0 +1,697 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/55/e4373e888fdacb15563ef6fa9fa8c8252476ea071e96fb46defac9f18bf2/aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745", size = 21977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/74/fbb6559de3607b3300b9be3cc64e97548d55678e44623db17820dbd20002/aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8", size = 14756 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, + { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, + { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, + { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, + { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, + { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, + { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, + { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, + { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, + { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, + { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, + { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, + { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, + { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, + { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982 }, + { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662 }, + { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950 }, + { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178 }, + { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939 }, + { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125 }, + { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176 }, + { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192 }, + { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296 }, + { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524 }, + { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471 }, + { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312 }, + { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783 }, + { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229 }, + { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "attrs" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, +] + +[[package]] +name = "blext" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "cyclopts" }, + { name = "pip" }, + { name = "pydantic" }, + { name = "pypdl" }, + { name = "rich" }, + { name = "tomli-w" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "cyclopts", specifier = ">=3.1.5" }, + { name = "pip", specifier = ">=24.3.1" }, + { name = "pydantic", specifier = ">=2.10.5" }, + { name = "pypdl", specifier = ">=1.5.1" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "tomli-w", specifier = ">=1.1.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "ruff", specifier = ">=0.9.1" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cyclopts" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4.0'" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/bd/dfb30190921dfed99b97ec4b94f0896a477cfbe9beb6997b304bf6424603/cyclopts-3.1.5.tar.gz", hash = "sha256:a17200237c8e8b0a827ac81295ae79ee349edb6fb0b9629dc43160c4f541854e", size = 60437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/40/594c8facedbb459cb2362e03b18e30490f7017afed592edd56391638060f/cyclopts-3.1.5-py3-none-any.whl", hash = "sha256:4bc1bdf2c7b04fb19bffea163634f3a02cba9becb883be88e073bec0c4d1b0bc", size = 69495 }, +] + +[[package]] +name = "docstring-parser" +version = "0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, + { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, + { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, + { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, + { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, + { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, + { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, + { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, + { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, + { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, + { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, + { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, + { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, + { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, + { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, + { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, + { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, + { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, + { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, + { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, + { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, + { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, + { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, + { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, + { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, + { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, + { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, + { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, + { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, + { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, + { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, + { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, + { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, + { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, + { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, + { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, + { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, + { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, + { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, + { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, + { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, + { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, + { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, + { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, + { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, + { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, + { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, + { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, + { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, + { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, + { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, + { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, + { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, + { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pip" +version = "24.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/b422acd212ad7eedddaf7981eee6e5de085154ff726459cf2da7c5a184c1/pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99", size = 1931073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7d/500c9ad20238fcfcb4cb9243eede163594d7020ce87bd9610c9e02771876/pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", size = 1822182 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/0f/2913b6791ebefb2b25b4efd4bb2299c985e09786b9f5b19184a88e5778dd/propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16", size = 79297 }, + { url = "https://files.pythonhosted.org/packages/cf/73/af2053aeccd40b05d6e19058419ac77674daecdd32478088b79375b9ab54/propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717", size = 45611 }, + { url = "https://files.pythonhosted.org/packages/3c/09/8386115ba7775ea3b9537730e8cf718d83bbf95bffe30757ccf37ec4e5da/propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3", size = 45146 }, + { url = "https://files.pythonhosted.org/packages/03/7a/793aa12f0537b2e520bf09f4c6833706b63170a211ad042ca71cbf79d9cb/propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9", size = 232136 }, + { url = "https://files.pythonhosted.org/packages/f1/38/b921b3168d72111769f648314100558c2ea1d52eb3d1ba7ea5c4aa6f9848/propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787", size = 239706 }, + { url = "https://files.pythonhosted.org/packages/14/29/4636f500c69b5edea7786db3c34eb6166f3384b905665ce312a6e42c720c/propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465", size = 238531 }, + { url = "https://files.pythonhosted.org/packages/85/14/01fe53580a8e1734ebb704a3482b7829a0ef4ea68d356141cf0994d9659b/propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af", size = 231063 }, + { url = "https://files.pythonhosted.org/packages/33/5c/1d961299f3c3b8438301ccfbff0143b69afcc30c05fa28673cface692305/propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7", size = 220134 }, + { url = "https://files.pythonhosted.org/packages/00/d0/ed735e76db279ba67a7d3b45ba4c654e7b02bc2f8050671ec365d8665e21/propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f", size = 220009 }, + { url = "https://files.pythonhosted.org/packages/75/90/ee8fab7304ad6533872fee982cfff5a53b63d095d78140827d93de22e2d4/propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54", size = 212199 }, + { url = "https://files.pythonhosted.org/packages/eb/ec/977ffaf1664f82e90737275873461695d4c9407d52abc2f3c3e24716da13/propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505", size = 214827 }, + { url = "https://files.pythonhosted.org/packages/57/48/031fb87ab6081764054821a71b71942161619549396224cbb242922525e8/propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82", size = 228009 }, + { url = "https://files.pythonhosted.org/packages/1a/06/ef1390f2524850838f2390421b23a8b298f6ce3396a7cc6d39dedd4047b0/propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca", size = 231638 }, + { url = "https://files.pythonhosted.org/packages/38/2a/101e6386d5a93358395da1d41642b79c1ee0f3b12e31727932b069282b1d/propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e", size = 222788 }, + { url = "https://files.pythonhosted.org/packages/db/81/786f687951d0979007e05ad9346cd357e50e3d0b0f1a1d6074df334b1bbb/propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034", size = 40170 }, + { url = "https://files.pythonhosted.org/packages/cf/59/7cc7037b295d5772eceb426358bb1b86e6cab4616d971bd74275395d100d/propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3", size = 44404 }, + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 }, +] + +[[package]] +name = "pydantic" +version = "2.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pypdl" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/5a/cd399b1b36fa5e1fc61f6d3a39aff12345aaac8e187073a5ba01e418a515/pypdl-1.5.1.tar.gz", hash = "sha256:975c2c8b58ab94154f2afb5308ffdd5e89f3944d3d149a9f1f6ba1de412a0355", size = 23944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/5e/ac68c41650d7b29f1c52e4fc0923f6a098a4070d44e340c3c570fb15005d/pypdl-1.5.1-py3-none-any.whl", hash = "sha256:e95d5ea48a56bf807b73b96ebd48e918648f7cb432dbbe8b8968f93d9ad47e76", size = 20712 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621 }, +] + +[[package]] +name = "ruff" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241 }, + { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960 }, + { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803 }, + { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929 }, + { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717 }, + { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921 }, + { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074 }, + { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093 }, + { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610 }, + { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273 }, + { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314 }, + { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982 }, + { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750 }, + { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331 }, + { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708 }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +]