From f1b4e0697a3a9a708098a0e3fa516a11ea33f972 Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Wed, 12 Nov 2025 17:42:07 -0500 Subject: [PATCH 01/10] Refactor Broker's logging for improved library and CLI usage Refactoring: - Migrated all codebase modules from `logzero` to the standard Python `logging` library. - Centralized logging configuration logic into a new `broker.logging` module. - Implemented a `RedactingFilter` to automatically redact sensitive information (e.g., passwords, tokens) from log records. - Reworked logging initialization within CLI commands (`broker.commands`) to ensure early setup and capture all messages, including those during module imports. Features: - Introduced support for structured JSON log files alongside existing text file logging, configurable via settings. - Added a custom `TRACE` log level (level 5) for highly verbose debugging. - Leveraged `rich` for enhanced, color-coded console output. - Included utility functions to disable `urllib3` warnings and patch `awxkit` to support trace-level logging of API calls. Configuration: - Extended `broker.settings` with new `LOGGING.LOG_PATH` and `LOGGING.STRUCTURED` options for fine-grained control over log file behavior. - Updated `pyproject.toml` to replace `logzero` with `python-json-logger` as a dependency. - Adjusted `pyproject.toml` and `tox.toml` pytest configurations, including new `ruff` per-file ignores to allow necessary early logging imports. Documentation: - Added a new "API Usage" section to `README.md`, detailing how to integrate Broker as a Python library and manage its logging via standard `logging` configuration. Tests: - Configured logging specifically for the test session within `conftest.py` to ensure proper log capture during testing. --- README.md | 27 + broker/binds/beaker.py | 3 +- broker/binds/foreman.py | 3 +- broker/binds/ssh2.py | 3 +- broker/binds/utils.py | 3 +- broker/broker.py | 16 +- broker/commands.py | 21 +- broker/config_manager.py | 4 +- broker/config_migrations/example_migration.py | 4 +- broker/config_migrations/v0_6_0.py | 4 +- broker/config_migrations/v0_6_12.py | 4 +- broker/config_migrations/v0_6_3.py | 4 +- broker/config_migrations/v0_6_9.py | 4 +- broker/exceptions.py | 2 +- broker/helpers.py | 14 +- broker/hosts.py | 4 +- broker/logging.py | 258 +++ broker/providers/__init__.py | 4 +- broker/providers/ansible_tower.py | 6 +- broker/providers/beaker.py | 4 +- broker/providers/container.py | 4 +- broker/providers/foreman.py | 4 +- broker/providers/openstack.py | 3 +- broker/session.py | 5 +- broker/settings.py | 2 + pyproject.toml | 30 +- tests/conftest.py | 9 + tox.toml | 4 - uv.lock | 1847 ++++++++++------- 29 files changed, 1489 insertions(+), 811 deletions(-) create mode 100644 broker/logging.py diff --git a/README.md b/README.md index dc91e949..4137bf0c 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,30 @@ Clone the Broker repository and install locally with `uv pip install "broker[de Copy the example settings file to `broker_settings.yaml` and edit it. To run Broker outside of its base directory, specify the directory with the `BROKER_DIRECTORY` environment variable. + + +# API Usage +TODO: Flesh this out + +## Using Broker as a Library + +When using Broker as a library in your Python code, you control logging configuration: + +```python +import logging + +# Configure logging for your application +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +# Optionally, control Broker's log level specifically +logging.getLogger('broker').setLevel(logging.DEBUG) + +# Now import and use Broker +from broker import Broker +broker = Broker() +``` + +Broker uses the standard Python logging hierarchy under the `broker.*` namespace. diff --git a/broker/binds/beaker.py b/broker/binds/beaker.py index e66b5550..c1c99171 100644 --- a/broker/binds/beaker.py +++ b/broker/binds/beaker.py @@ -1,12 +1,13 @@ """A wrapper around the Beaker CLI.""" import json +import logging from pathlib import Path import subprocess import time from xml.etree import ElementTree as ET -from logzero import logger +logger = logging.getLogger(__name__) from broker import helpers from broker.exceptions import BeakerBindError diff --git a/broker/binds/foreman.py b/broker/binds/foreman.py index 6e8664ee..14da6af3 100644 --- a/broker/binds/foreman.py +++ b/broker/binds/foreman.py @@ -1,8 +1,9 @@ """Foreman provider implementation.""" +import logging import time -from logzero import logger +logger = logging.getLogger(__name__) import requests from broker import exceptions diff --git a/broker/binds/ssh2.py b/broker/binds/ssh2.py index abb56c6f..532f935d 100644 --- a/broker/binds/ssh2.py +++ b/broker/binds/ssh2.py @@ -9,9 +9,10 @@ """ from contextlib import contextmanager +import logging from pathlib import Path -from logzero import logger +logger = logging.getLogger(__name__) from ssh2 import sftp as _sftp from ssh2.exceptions import SocketSendError from ssh2.session import Session as _Session diff --git a/broker/binds/utils.py b/broker/binds/utils.py index 4ecd3250..5d70ec94 100644 --- a/broker/binds/utils.py +++ b/broker/binds/utils.py @@ -1,8 +1,9 @@ """Module providing base SSH methods and classes.""" +import logging import socket -from logzero import logger +logger = logging.getLogger(__name__) from broker import exceptions diff --git a/broker/broker.py b/broker/broker.py index 097c9210..18674abb 100644 --- a/broker/broker.py +++ b/broker/broker.py @@ -19,14 +19,16 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from contextlib import contextmanager +import logging -from logzero import logger - -from broker import exceptions, helpers, logger as broker_logger +from broker import exceptions, helpers from broker.hosts import Host +from broker.logging import setup_logging from broker.providers import PROVIDER_ACTIONS, PROVIDERS, _provider_imports from broker.settings import clone_global_settings +logger = logging.getLogger(__name__) + # load all the provider class so they are registered for _import in _provider_imports: __import__(f"broker.providers.{_import}", globals(), locals(), [], 0) @@ -58,11 +60,11 @@ def __init__(self, broker_settings=None, **kwargs): if broker_settings: logger.debug(f"Using local settings object: {self._settings.to_dict()}") if "logging" in broker_settings: - broker_logger.setup_logzero( - level=broker_settings.logging.console_level, - formatter=kwargs.pop("broker_log_formatter", None), + setup_logging( + console_level=broker_settings.logging.console_level, file_level=broker_settings.logging.file_level, - path=broker_settings.logging.get("file_path"), + log_path=broker_settings.logging.log_path, + structured=broker_settings.logging.structured, ) else: logger.debug("Using global settings.") diff --git a/broker/commands.py b/broker/commands.py index 721f54b5..3997b7b3 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -1,10 +1,16 @@ """Defines the CLI commands for Broker.""" from functools import wraps +import logging import signal import sys -from logzero import logger +# CRITICAL: Import and setup basic logging BEFORE any other broker imports +# This captures import-time logs from metaclasses and module initialization +from broker.logging import setup_logging + +setup_logging(console_level=logging.INFO) # Basic setup until settings are loaded + from rich.console import Console from rich.syntax import Syntax from rich.table import Table @@ -13,9 +19,20 @@ from broker import exceptions, helpers, settings from broker.broker import Broker from broker.config_manager import ConfigManager -from broker.logger import LOG_LEVEL +from broker.logging import LOG_LEVEL from broker.providers import PROVIDER_ACTIONS, PROVIDER_HELP, PROVIDERS +# Now configure logging with actual settings +setup_logging( + console_level=settings.settings.logging.console_level, + file_level=settings.settings.logging.file_level, + log_path=settings.settings.logging.log_path, + structured=settings.settings.logging.structured, +) + +# Get logger for this module +logger = logging.getLogger(__name__) + signal.signal(signal.SIGINT, helpers.handle_keyboardinterrupt) CONSOLE = Console(no_color=settings.settings.less_colors) # rich console for pretty printing diff --git a/broker/config_manager.py b/broker/config_manager.py index b7cbc651..47d1fc20 100644 --- a/broker/config_manager.py +++ b/broker/config_manager.py @@ -3,18 +3,20 @@ import importlib from importlib.metadata import version import json +import logging import os from pathlib import Path import pkgutil import sys import click -from logzero import logger from packaging.version import Version from ruamel.yaml import YAML, YAMLError from broker import exceptions +logger = logging.getLogger(__name__) + yaml = YAML() yaml.default_flow_style = False yaml.sort_keys = False diff --git a/broker/config_migrations/example_migration.py b/broker/config_migrations/example_migration.py index fb829766..86564d27 100644 --- a/broker/config_migrations/example_migration.py +++ b/broker/config_migrations/example_migration.py @@ -6,7 +6,9 @@ e.g. cp example_migration.py v0_6_1.py """ -from logzero import logger +import logging + +logger = logging.getLogger(__name__) TO_VERSION = "0.6.1" diff --git a/broker/config_migrations/v0_6_0.py b/broker/config_migrations/v0_6_0.py index 72e464cd..a00fbfc1 100644 --- a/broker/config_migrations/v0_6_0.py +++ b/broker/config_migrations/v0_6_0.py @@ -1,6 +1,8 @@ """Config migrations for versions older than 0.6.0 to 0.6.0.""" -from logzero import logger +import logging + +logger = logging.getLogger(__name__) TO_VERSION = "0.6.0" diff --git a/broker/config_migrations/v0_6_12.py b/broker/config_migrations/v0_6_12.py index 38be5a29..f6263556 100644 --- a/broker/config_migrations/v0_6_12.py +++ b/broker/config_migrations/v0_6_12.py @@ -1,6 +1,8 @@ """Config migrations for versions older than 0.6.12 to 0.6.12.""" -from logzero import logger +import logging + +logger = logging.getLogger(__name__) TO_VERSION = "0.6.12" diff --git a/broker/config_migrations/v0_6_3.py b/broker/config_migrations/v0_6_3.py index 997d23ee..05885835 100644 --- a/broker/config_migrations/v0_6_3.py +++ b/broker/config_migrations/v0_6_3.py @@ -1,6 +1,8 @@ """Config migrations for versions older than 0.6.3 to 0.6.3.""" -from logzero import logger +import logging + +logger = logging.getLogger(__name__) TO_VERSION = "0.6.3" diff --git a/broker/config_migrations/v0_6_9.py b/broker/config_migrations/v0_6_9.py index 7f5676e7..8234c917 100644 --- a/broker/config_migrations/v0_6_9.py +++ b/broker/config_migrations/v0_6_9.py @@ -1,6 +1,8 @@ """Config migrations for versions older than 0.6.9 to 0.6.9.""" -from logzero import logger +import logging + +logger = logging.getLogger(__name__) TO_VERSION = "0.6.9" diff --git a/broker/exceptions.py b/broker/exceptions.py index f212ce73..e33d05e8 100644 --- a/broker/exceptions.py +++ b/broker/exceptions.py @@ -2,7 +2,7 @@ import logging -from logzero import logger +logger = logging.getLogger(__name__) class BrokerError(Exception): diff --git a/broker/helpers.py b/broker/helpers.py index a994daa8..a7238b93 100644 --- a/broker/helpers.py +++ b/broker/helpers.py @@ -9,6 +9,7 @@ import inspect from io import BytesIO import json +import logging import os from pathlib import Path import sys @@ -18,13 +19,14 @@ from uuid import uuid4 import click -from logzero import logger from rich.table import Table from ruamel.yaml import YAML from broker import exceptions from broker.settings import clone_global_settings +logger = logging.getLogger(__name__) + FilterTest = namedtuple("FilterTest", "haystack needle test") INVENTORY_LOCK = threading.Lock() @@ -511,10 +513,9 @@ def update_log_level(ctx, param, value): param: The Click parameter object. value: The new log level value. """ - from broker import logger as b_log + from broker.logging import setup_logging - b_log.set_log_level(value) - b_log.set_file_logging(value) + setup_logging(console_level=value) def set_emit_file(ctx, param, value): @@ -528,10 +529,9 @@ def fork_broker(): if pid: logger.info(f"Running broker in the background with pid: {pid}") sys.exit(0) - from broker import logger as b_log + from broker.logging import setup_logging - b_log.set_log_level("silent") - b_log.set_file_logging("silent") + setup_logging(console_level="silent", file_level="silent") def handle_keyboardinterrupt(*args): diff --git a/broker/hosts.py b/broker/hosts.py index fbcd8b2d..344f5391 100644 --- a/broker/hosts.py +++ b/broker/hosts.py @@ -15,10 +15,12 @@ ``` """ -from logzero import logger +import logging from broker.exceptions import HostError, NotImplementedError +logger = logging.getLogger(__name__) + SETTINGS_VALIDATED = False diff --git a/broker/logging.py b/broker/logging.py new file mode 100644 index 00000000..76081bc3 --- /dev/null +++ b/broker/logging.py @@ -0,0 +1,258 @@ +"""Modern logging configuration for Broker. + +This module provides logging setup that supports two distinct modes: +1. CLI Mode: Beautiful rich console output + structured JSON file logging +2. Library Mode: No handler configuration (consumers handle their own logging) +""" + +import copy +from enum import IntEnum +import logging +from pathlib import Path + +import click +from pythonjsonlogger import jsonlogger +from rich.logging import RichHandler + +from broker.settings import BROKER_DIRECTORY + + +class _LoggingState: + """Class to hold current logging state.""" + + console_level = "info" + file_level = "debug" + log_path = "logs/broker.log" + structured = False + + +class LOG_LEVEL(IntEnum): + """Log levels with custom TRACE level.""" + + TRACE = 5 + DEBUG = logging.DEBUG + INFO = logging.INFO + WARNING = logging.WARNING + ERROR = logging.ERROR + + +class RedactingFilter(logging.Filter): + """Custom logging.Filter to redact secrets from log records.""" + + def __init__(self, sensitive): + super().__init__() + self._sensitive = sensitive + + def filter(self, record): + """Filter the record and redact the sensitive keys.""" + if isinstance(record.args, dict): + record.args = self.redact_sensitive(record.args) + else: + record.args = tuple(self.redact_sensitive(arg) for arg in record.args) + return True + + def redact_sensitive(self, data): + """Recursively redact sensitive data.""" + if isinstance(data, list | tuple): + data_copy = [self.redact_sensitive(item) for item in data] + elif isinstance(data, dict): + data_copy = copy.deepcopy(data) + for k, v in data_copy.items(): + if isinstance(v, dict | list): + data_copy[k] = self.redact_sensitive(v) + elif k in self._sensitive and v: + data_copy[k] = "******" + else: + data_copy = data + return data_copy + + +# Register custom TRACE level +logging.addLevelName(LOG_LEVEL.TRACE, "TRACE") + +# Sensitive fields to redact +_SENSITIVE_FIELDS = ["password", "pword", "token", "host_password"] + + +def resolve_log_level(level): + """Resolve log level from string or int to LOG_LEVEL enum.""" + if isinstance(level, int): + # Map standard logging levels to our enum + for log_level in LOG_LEVEL: + if log_level.value == level: + return log_level + return LOG_LEVEL.INFO + + if isinstance(level, str): + try: + return LOG_LEVEL[level.upper()] + except KeyError: + return LOG_LEVEL.INFO + + return LOG_LEVEL.INFO + + +def setup_logging( + console_level=None, + file_level=None, + log_path=None, + structured=None, +): + """Configure logging for Broker CLI mode. + + This function should ONLY be called by the CLI entry point. + When broker is used as a library, logging should not be configured. + + Args: + console_level: Logging level for console output (string or logging level) + file_level: Logging level for file output (string or logging level) + log_path: Path to log file (can be directory or full file path) + structured: If True, create additional JSON log file alongside text logs + """ + # Use existing settings if parameters are None + if console_level is None: + console_level = _LoggingState.console_level + if file_level is None: + file_level = _LoggingState.file_level + if log_path is None: + log_path = _LoggingState.log_path + if structured is None: + structured = _LoggingState.structured + + # Save current settings for future calls + _LoggingState.console_level = console_level + _LoggingState.file_level = file_level + _LoggingState.log_path = log_path + _LoggingState.structured = structured + + root_logger = logging.getLogger() + # Clear any existing handlers to avoid duplicates on reconfiguration + root_logger.handlers.clear() + + # Resolve log levels + console_log_level = resolve_log_level(console_level) + file_log_level = resolve_log_level(file_level) + + # Set root logger to lowest level so handlers can filter + root_logger.setLevel(min(console_log_level.value, file_log_level.value)) + + # Add redacting filter to root logger + root_logger.addFilter(RedactingFilter(_SENSITIVE_FIELDS)) + + # Skip handler setup if level is "silent" + if console_level != "silent": + # Console handler with rich formatting + console_handler = RichHandler( + rich_tracebacks=True, + tracebacks_suppress=[click], + show_time=True, + show_path=console_log_level <= LOG_LEVEL.DEBUG, + markup=True, + ) + console_handler.setLevel(console_log_level.value) + + # Use more verbose format for debug/trace + if console_log_level <= LOG_LEVEL.DEBUG: + console_format = "%(message)s" + else: + console_format = "%(message)s" + + console_handler.setFormatter(logging.Formatter(console_format, datefmt="%d%b %H:%M:%S")) + root_logger.addHandler(console_handler) + + # Text file handler (always created unless file_level is "silent") + if file_level != "silent": + # Resolve log path + log_file_path = Path(log_path) + if not log_file_path.is_absolute(): + log_file_path = BROKER_DIRECTORY / log_path + + # If path is a directory, append default filename + if log_file_path.suffix == "": + log_file_path = log_file_path / "broker.log" + + # Ensure parent directory exists + log_file_path.parent.mkdir(parents=True, exist_ok=True) + + # Create rotating file handler for text logs + from logging.handlers import RotatingFileHandler + + text_file_handler = RotatingFileHandler( + log_file_path, + maxBytes=int(1e9), # 1GB + backupCount=3, + ) + text_file_handler.setLevel(file_log_level.value) + + # Standard text logging + text_file_handler.setFormatter( + logging.Formatter( + "[%(levelname)s %(asctime)s %(name)s:%(lineno)d] %(message)s", + datefmt="%d%b %H:%M:%S", + ) + ) + root_logger.addHandler(text_file_handler) + + # JSON file handler (additional, if structured logging is enabled) + if structured: + # Create JSON log file path by replacing extension + json_log_path = log_file_path.with_suffix(log_file_path.suffix + ".json") + + json_file_handler = RotatingFileHandler( + json_log_path, + maxBytes=int(1e9), # 1GB + backupCount=3, + ) + json_file_handler.setLevel(file_log_level.value) + + # JSON structured logging + json_file_handler.setFormatter( + jsonlogger.JsonFormatter( + "%(asctime)s %(name)s %(levelname)s %(message)s", + rename_fields={"asctime": "timestamp", "levelname": "level", "name": "logger"}, + datefmt="%d%b %H:%M:%S", + ) + ) + root_logger.addHandler(json_file_handler) + + +def try_disable_urllib3_warnings(): + """Attempt to disable urllib3 InsecureRequestWarning if urllib3 is available.""" + try: + import urllib3 + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + except ImportError: + pass + + +def try_patch_awx_for_verbosity(): + """Patch the awxkit API to enable trace-level logging of API calls.""" + try: + from awxkit import api + except ImportError: + return + + awx_log = logging.getLogger("awxkit.api") + + def patch(cls, name): + func = getattr(cls, name) + + def the_patch(self, *args, **kwargs): + awx_log.log(LOG_LEVEL.TRACE.value, f"Calling {self=} {func=}(*{args=}, **{kwargs=}") + retval = func(self, *args, **kwargs) + awx_log.log( + LOG_LEVEL.TRACE.value, + f"Finished {self=} {func=}(*{args=}, **{kwargs=}) {retval=}", + ) + return retval + + setattr(cls, name, the_patch) + + for method in "delete get head options patch post put".split(): + patch(api.Connection, method) + + +# Apply patches on module import (these are safe for library mode) +try_disable_urllib3_warnings() +try_patch_awx_for_verbosity() diff --git a/broker/providers/__init__.py b/broker/providers/__init__.py index 6956b1fd..09f9ca1f 100644 --- a/broker/providers/__init__.py +++ b/broker/providers/__init__.py @@ -33,14 +33,16 @@ def get_inventory(self, **inventory_opts): from abc import ABCMeta, abstractmethod import inspect +import logging from pathlib import Path import dynaconf -from logzero import logger from broker import exceptions from broker.settings import clone_global_settings +logger = logging.getLogger(__name__) + # populate a list of all provider module names _provider_imports = [ f.stem for f in Path(__file__).parent.glob("*.py") if f.is_file() and f.stem != "__init__" diff --git a/broker/providers/ansible_tower.py b/broker/providers/ansible_tower.py index 054570ba..7f64ffc6 100644 --- a/broker/providers/ansible_tower.py +++ b/broker/providers/ansible_tower.py @@ -3,6 +3,7 @@ from functools import cache, cached_property import inspect import json +import logging import os import random import string @@ -12,7 +13,8 @@ import click from dynaconf import Validator -from logzero import logger + +logger = logging.getLogger(__name__) from packaging.version import InvalidVersion, Version from requests.exceptions import ConnectionError from rich.console import Console @@ -670,7 +672,7 @@ def _parse_string_value(value): parsed = json.loads(value) # Only return parsed value if it's a complex type (list/dict) # Keep simple values as strings to preserve user intent - if isinstance(parsed, (list, dict)): + if isinstance(parsed, list | dict): return parsed except (json.JSONDecodeError, ValueError): pass diff --git a/broker/providers/beaker.py b/broker/providers/beaker.py index 418e6e73..f0d413ce 100644 --- a/broker/providers/beaker.py +++ b/broker/providers/beaker.py @@ -1,10 +1,12 @@ """Beaker provider implementation.""" import inspect +import logging import click from dynaconf import Validator -from logzero import logger + +logger = logging.getLogger(__name__) from broker import helpers from broker.binds.beaker import BeakerBind diff --git a/broker/providers/container.py b/broker/providers/container.py index 3b98bfbf..86d61559 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -3,11 +3,13 @@ from functools import cache import getpass import inspect +import logging from uuid import uuid4 import click from dynaconf import Validator -from logzero import logger + +logger = logging.getLogger(__name__) from broker import exceptions, helpers from broker.binds import containers diff --git a/broker/providers/foreman.py b/broker/providers/foreman.py index 18bad02a..391fc8a4 100644 --- a/broker/providers/foreman.py +++ b/broker/providers/foreman.py @@ -1,11 +1,13 @@ """Foreman provider implementation.""" import inspect +import logging from uuid import uuid4 import click from dynaconf import Validator -from logzero import logger + +logger = logging.getLogger(__name__) from broker.binds import foreman from broker.helpers import Result diff --git a/broker/providers/openstack.py b/broker/providers/openstack.py index b3c2a1c2..4a1fb6e7 100644 --- a/broker/providers/openstack.py +++ b/broker/providers/openstack.py @@ -7,7 +7,8 @@ import click from dynaconf import Validator -from logzero import logger + +logger = logging.getLogger(__name__) from broker import exceptions, helpers from broker.providers import Provider diff --git a/broker/session.py b/broker/session.py index 04c2512a..1fafe25c 100644 --- a/broker/session.py +++ b/broker/session.py @@ -11,15 +11,16 @@ from contextlib import contextmanager from importlib.metadata import entry_points +import logging from pathlib import Path import tempfile -from logzero import logger - from broker import helpers from broker.exceptions import NotImplementedError from broker.settings import clone_global_settings +logger = logging.getLogger(__name__) + SSH_NOT_INSTALLED_MSG = ( "{backend} is not installed.\n" "ssh actions will not work.\n" diff --git a/broker/settings.py b/broker/settings.py index 6346a66f..a3022cb1 100644 --- a/broker/settings.py +++ b/broker/settings.py @@ -54,6 +54,8 @@ is_in=["error", "warning", "info", "debug", "trace", "silent"], default="debug", ), + Validator("LOGGING.LOG_PATH", default="logs/broker.log"), + Validator("LOGGING.STRUCTURED", default=False), Validator("THREAD_LIMIT", default=None), Validator("INVENTORY_FIELDS", is_type_of=dict), Validator("INVENTORY_LIST_VARS", is_type_of=str, default="hostname | name"), diff --git a/pyproject.toml b/pyproject.toml index 637c1b6c..c7baa956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "broker" -version = "0.7.3" +version = "0.8.0" description = "The infrastructure middleman." readme = "README.md" requires-python = ">=3.10" @@ -24,8 +24,8 @@ classifiers = [ dependencies = [ "click", "dynaconf>=3.1.6,<4.0.0", - "logzero", "packaging", + "python-json-logger", "requests", "rich", "rich_click", @@ -39,7 +39,16 @@ Repository = "https://github.com/SatelliteQE/broker" ansible_pylibssh = ["ansible-pylibssh"] ansibletower = ["awxkit"] beaker = ["beaker-client"] -dev = ["docker", "pexpect", "pre-commit", "pytest", "pytest-randomly", "ruff"] +dev = [ + "docker", + "pexpect", + "pre-commit", + "pytest", + "pytest-randomly", + "ruff", + "tox", + "tox-uv", +] docker = ["docker", "paramiko"] hussh = ["hussh>=0.1.7"] openstack = ["openstacksdk"] @@ -83,7 +92,15 @@ paramiko = "broker.binds.paramiko:InteractiveShell" [tool.pytest.ini_options] testpaths = ["tests"] -addopts = ["-v", "-l", "--color=yes", "--code-highlight=yes"] +addopts = [ + "-v", + "-l", + "--color=yes", + "--code-highlight=yes", + "--ignore=tests/functional", + "--ignore=tests/test_ssh.py", +] +norecursedirs = ["broker", ".tox", ".git", "*.egg"] [tool.ruff] line-length = 100 @@ -167,6 +184,7 @@ ignore = [ "D413", # Missing blank line after last section "E501", # line too long "E731", # do not assign a lambda expression, use a def + "G004", # Logging statement uses f-string (modern Python accepts this) "PLC0415", # ignore top-level import restrictions "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "RUF012", # Mutable class attributes should be annotated with typing.ClassVar @@ -182,7 +200,9 @@ known-first-party = ["broker"] combine-as-imports = true [tool.ruff.lint.per-file-ignores] -# None at this time +"broker/commands.py" = ["E402"] # Intentional: logging setup before imports +"broker/providers/*.py" = ["E402"] # logger defined before other imports +"broker/binds/*.py" = ["E402"] # logger defined before other imports [tool.ruff.lint.mccabe] max-complexity = 25 diff --git a/tests/conftest.py b/tests/conftest.py index 102e6774..f92d228b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,15 @@ def pytest_sessionstart(session): """For things that need to happen before Broker is loaded.""" os.environ["BROKER_NO_GLOBAL_CONFIG"] = "True" + + # Set up logging for the test session + from broker.logging import setup_logging + setup_logging( + console_level="warning", + file_level="debug", + log_path="logs/broker_tests.log", + structured=False, + ) @pytest.fixture(scope="session") diff --git a/tox.toml b/tox.toml index 763399a1..1243b565 100644 --- a/tox.toml +++ b/tox.toml @@ -47,10 +47,6 @@ commands = [ "pytest", "-v", "{tox_root}/tests/", - "--ignore", - "{tox_root}/tests/functional", - "--ignore", - "{tox_root}/tests/test_ssh.py", "{posargs}", ], ] diff --git a/uv.lock b/uv.lock index f738439c..066c3639 100644 --- a/uv.lock +++ b/uv.lock @@ -4,42 +4,25 @@ requires-python = ">=3.10" [[package]] name = "ansible-pylibssh" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/93/50/38298568fbd517dcb7b08c06f4fbfc0481ab9eae1e4b01920495dacf672a/ansible-pylibssh-1.2.2.tar.gz", hash = "sha256:753e570dcdceb6ab8e362e91cc0d5993beebc93d287b88178db55509f6423ab5", size = 135490, upload-time = "2024-06-27T17:45:14.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/40/7bb4ac4b77f8db736dc73d3786af56417aad4592941bb8f13ca5e7c56364/ansible_pylibssh-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b2c011ca038984fc244b8468e3a67c900e66b1a69549d8b9d3815d60f331bcdd", size = 2291421, upload-time = "2024-06-27T17:41:11.538Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ac/5d451d8c3308e10ea9144bb1ecb40472b072573b6bce1660f47ef5d53db3/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:acf52d4f3bb15c36c848ba5184f8d4278232470aa56ebaa4edb359b300c37183", size = 2687415, upload-time = "2024-06-27T17:41:15.135Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d5/cc6ab1ff3dd64ce5a95140d411545509d9dd13baa58731978b56baf6a41a/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01dd6c016abf258d3a6f983c29918ac4af13d8744124b007f3d620c393e7c67a", size = 2660572, upload-time = "2024-06-27T17:41:18.529Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f2/b9e16942f82e394873d0600d4a59b7bb94948aa3e04cfb61e385e714d3fd/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cda0c6d5bfdac54aa6bb9d6865aa8c19930f96dddccb121ec67d53e5eb0f9cc8", size = 2936944, upload-time = "2024-06-27T17:41:21.234Z" }, - { url = "https://files.pythonhosted.org/packages/db/c4/d9603973cc665089751f582c82e60f3725397d5803e14f9b1aa867ec0641/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50d6eca4048683d36939d8dcc66b367151231d28b7454c79e279b2d51b8057d7", size = 2611725, upload-time = "2024-06-27T17:41:23.796Z" }, - { url = "https://files.pythonhosted.org/packages/7b/09/8c7cb1c60d25112c0f361f80d10ec0b062a770ad60c38aeb35379cf52a82/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efc45e6db2329e3728210e795158682b8da4da054159c71e35100d0c8e74efa", size = 2758105, upload-time = "2024-06-27T17:41:25.86Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/5643d7aaebe24817a6ee621e5dd884a4a5991fd8937794c07cec4f160e95/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:8577f23adb1bc9c9a233cc316ae8d92ff51ae77c62d40eabcafe3190be6405ec", size = 2259326, upload-time = "2024-06-27T17:41:28.771Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5a/f4a1a84e3e2a999d80f4eb8e0ab83b415bbbf65de0eb10607c1e4334bb6d/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:6f8365325006e644f922db447128ced76955ee5eb334c741a9437bb42e7306d4", size = 2425682, upload-time = "2024-06-27T17:41:30.745Z" }, - { url = "https://files.pythonhosted.org/packages/44/87/3ef182be968c5dd2e862cfd1d8c8e8251edb38e422dd9e01db60e1c0502a/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_24_s390x.whl", hash = "sha256:b13edc7b9f14d82112231f7088f4e5e1f6198a84ee52b8e50c9af8771b0976e5", size = 2336962, upload-time = "2024-06-27T17:41:32.851Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/adafafe0e4a5e258da06700fc5b5d4ace25a12eea524f82009dc2a9bcb54/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:d4f776041e93e2113efd939a076ecbfb9e9b0bfcbe5a059d1b62ba54c6a00aaf", size = 2483347, upload-time = "2024-06-27T17:41:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/6810fb36dca68fa68c55f2ab4146e5419c4eef1b3d7dcc6b8e32c95fbba3/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0164a24b4fe96a87c37b3f542dfa15e6e8e102ddd609389d9ee58c94bf8056df", size = 2654830, upload-time = "2024-06-27T17:41:38.493Z" }, - { url = "https://files.pythonhosted.org/packages/ec/99/d10129a41ffbf2aee072103ad32284570aa87b0e7a1f4e7d3e9d5f1a69d2/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:db723e5042e595d1980644250da3d7d4d6c2a019a9ae3cf609f4091ba1640bd0", size = 2961612, upload-time = "2024-06-27T17:41:41.1Z" }, - { url = "https://files.pythonhosted.org/packages/6e/97/597fe4a8d3952395fbc9a56e68cc32815cc429f7a9c8530490bb526c3aa5/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:c61c6677b93929b664a9816dd5186ddc62efe938b5276b96e04ff283184c3955", size = 2655293, upload-time = "2024-06-27T17:41:43.911Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d7/828798082ebfd3e475d01d9f482c174347036e258fcc2225f7a7a4a705c7/ansible_pylibssh-1.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0fe729c64c339a7c45cdbbfdceb0eab22965bde125e20de33bf31cd8986c08e8", size = 2769891, upload-time = "2024-06-27T17:41:46.048Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e0/9e2ac18b1b9d6a4e665eab107986c54af4ef96c10e32f248cb3693122cff/ansible_pylibssh-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e31ba268b284d2a70135abc0410e3fd65d6591a031ad0ade352215f1fc3c10d6", size = 2292763, upload-time = "2024-06-27T17:41:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/04/35/be95b8cce02da57ccf3769fcc3e8095186c1ffa9ddf5d8f12212829afab0/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b28c8aeb1eed77b3c0b054c02c199fceb7dbbe38e991106b31c721eb468549de", size = 2793439, upload-time = "2024-06-27T17:41:50.096Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b0/40f1d7237c3c266e4a194e4fe572afc268d74f441d3f1690b323fc8c3e8b/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f1bcf3f58e75e94438f4d8719a47e8debede50f301e878c02a515ea0de8fa6a", size = 3070191, upload-time = "2024-06-27T17:41:52.308Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/ec76cc406e6176728b76696fd0d907867f41a9661f0b61a0202c8339c964/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:374b76b09f8044e7ecc6fbacec572226dcf5c9d8845bb367240e59fc7c16f1e3", size = 2739361, upload-time = "2024-06-27T17:41:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f9/d83567aa57b8c4a77e6612ad457852c59142fa4a91f5e492d9f85563036e/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67ea1eccfc921f2f348fb876a6dd63c2cd0f2aab8ca22d8f68cfeaa637372949", size = 2882444, upload-time = "2024-06-27T17:41:56.434Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/e2852a6a1e7ecac09b2f47c32833f85dd40962fb822e93a9787f0c9512f3/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:b7d3f91546dddb33368990ffa5acca8518c36dc5a36452bb7e62ea3961e0ec58", size = 2285188, upload-time = "2024-06-27T17:41:58.561Z" }, - { url = "https://files.pythonhosted.org/packages/55/86/142596d43820635f5b4a69d6da2332a47bbf3e408084d0d6625bb249ec0b/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:352148d0852557a73490c6f904b9cb658a322ca0ddf29404de58734bc84c5989", size = 2458406, upload-time = "2024-06-27T17:42:00.505Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c6/6d4f4d723002bfa9511bb60fdb579e25f180a538d2362918c4fcac0df3ae/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:965de2a436ecd44638f0f486af9dcc6cf88307bfccc01ebb8135413c05649433", size = 2362607, upload-time = "2024-06-27T17:42:02.645Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7d/184f9ec80964c02be9796fac5cfe5e4dc010ecd973a5cf81cb8cee3c2c55/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:1af727a8bf21ae4600e2024ce25f2faf8442b364900cdf8c0133d0d5add0ab00", size = 2518258, upload-time = "2024-06-27T17:42:04.876Z" }, - { url = "https://files.pythonhosted.org/packages/91/30/d0d95207b7e23cf53fe69ef99f87d7ed89907c72dac251b0d2b8d182560e/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1ca9ede0fb3fa80a2d394fa02169afa834802e58785a78b9be19e3f23a84776c", size = 2795498, upload-time = "2024-06-27T17:42:07.349Z" }, - { url = "https://files.pythonhosted.org/packages/84/5e/662b681b216d07c227a113ba59db01b899cfe1f970d43cba8adbeed6038f/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:73e9259d371b7b087e343ecd9db8d5db740486970a114ebec4ba8d2e7a2f3162", size = 3087475, upload-time = "2024-06-27T17:42:09.837Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a3/2c300a9436b4290a50f970eac9f03413f9a5e5aea0833a87e4df987d7b9d/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e89f876b2bcd4154e0eecc814aab60a5d1b75dcae380305c55493132af4d9175", size = 2785481, upload-time = "2024-06-27T17:42:12.827Z" }, - { url = "https://files.pythonhosted.org/packages/93/ad/4b5bf86a4c6882f43d259684bded42620c705c92e86f992022f41acfbd95/ansible_pylibssh-1.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3ead592ffbe8a97c3ba09c106066f9f494983183e4a08951ca93063d775e2ba7", size = 2906941, upload-time = "2024-06-27T17:42:15.699Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ad/69182877e961c41c8d85fea497de5d99c229cf30c47dcd7bc84362299e1d/ansible_pylibssh-1.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b9684d0dfc557a7c882cbcbdba8cd478d30a4009804d33067900c2956d4ffbd", size = 2294870, upload-time = "2024-06-27T17:42:17.854Z" }, - { url = "https://files.pythonhosted.org/packages/a8/06/7dbaabd8910d0acb96da28623cac8f55318089be0d1d6b2397c32f6c73da/ansible_pylibssh-1.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1b78d78a6b1c10404c1d141fc78fc18a2ca5290960ff55a39ef7d2d7a7701d8b", size = 2815358, upload-time = "2024-06-27T17:42:19.99Z" }, - { url = "https://files.pythonhosted.org/packages/24/12/b1cf2305890b3569eb476270c42e67c348d10a5ac66e21250f67cad3e927/ansible_pylibssh-1.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:e3ab210a012169339220640e9005188d00195cd8cb35036f2a25471eec65745c", size = 3087410, upload-time = "2024-06-27T17:42:22.001Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c1/b92aa0a4adf2e1f542a7587af88a5662949c5fa74791bff79333c9e0d3d8/ansible_pylibssh-1.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:36674b80d2c0a4c411385dcf9709b96a575bffd54cca81a82aaf572015001146", size = 2804564, upload-time = "2024-06-27T17:42:24.312Z" }, - { url = "https://files.pythonhosted.org/packages/5b/9a/e4a7787950df2876ff4e83767f7afd29b4f6f451e1eb2285fea9bdac38fb/ansible_pylibssh-1.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:43b2f708af82d57ea3df8d493590720a3dd7dc60a58c65b46eb97d94462be07c", size = 2934402, upload-time = "2024-06-27T17:42:26.839Z" }, +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/6d/93b13182acd29632ef3ab58650fc5727936942e2ad642a48f6a5e12aebb4/ansible-pylibssh-1.3.0.tar.gz", hash = "sha256:243ea1b0962b0b6b1e717ac0e69dac9636e61ec65b37260c317b2360c6e30ca7", size = 151535, upload-time = "2025-10-12T01:09:16.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/2e/0591e3f88b7cf9a3ad77147c93cd6eaa2fc4a801f07efbb215e748c061db/ansible_pylibssh-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fe5231259d23eaedf92c50836bcca2033270f8a753e86293cec472d8bc2b78c3", size = 2489774, upload-time = "2025-10-12T01:08:39.331Z" }, + { url = "https://files.pythonhosted.org/packages/42/c0/cb997bc56a08c7a688e8c441ba72e856ae685ffc9609057b183808a6df2d/ansible_pylibssh-1.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:206adcc43d638fdd2c71de180fcb3a5f4e6523278022b182c2d85624fe368823", size = 2361425, upload-time = "2025-10-12T01:08:41.609Z" }, + { url = "https://files.pythonhosted.org/packages/36/27/7cd6296958de769bcc9a1597678821098a3a0d063f4afb3531c04665f781/ansible_pylibssh-1.3.0-cp310-cp310-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddddb5bdafa0f01f6290e75ed8764b62610528e28c7b964f4f9acad8c88c9ac1", size = 2714233, upload-time = "2025-10-12T01:08:43.507Z" }, + { url = "https://files.pythonhosted.org/packages/30/fd/79c2cf8001dc5f27d56855a53e53da2edadeac981e4ab1e1b6cbf9e51a17/ansible_pylibssh-1.3.0-cp310-cp310-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ba088dcdb81bbc3c378b3bb1f847df1ab69bf99c8a7e5e0b052e3c5a9183adcc", size = 2194959, upload-time = "2025-10-12T01:08:45.881Z" }, + { url = "https://files.pythonhosted.org/packages/33/b4/befaa1f0ddb22dd3849d8466154bd6f3fd5e70fb3eb82cecb9e9ad62f9b0/ansible_pylibssh-1.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196ff3ed8a6f54e809703209f6a1096b2d85031b7eb2ceb0c6af1ab549a71acd", size = 2589618, upload-time = "2025-10-12T01:08:47.755Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8b/a9803748b30b4f2e6594440fd976ad20626ab02de334f2fc874428794977/ansible_pylibssh-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:248f9fc4687e07818942d618ba3133aa155402be9c0e31fb4df0d9c5504e1b64", size = 2490652, upload-time = "2025-10-12T01:08:49.434Z" }, + { url = "https://files.pythonhosted.org/packages/de/0e/19916cd0b7d626910ad6a4dc2ed88d9744a1bd1cda5672eac19d1bda4bab/ansible_pylibssh-1.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a6ad801fd94be7782abaa86ae0421b99c55586eb2f583b113a5b75964c823d7", size = 2357722, upload-time = "2025-10-12T01:08:51.293Z" }, + { url = "https://files.pythonhosted.org/packages/e6/57/3a79733b74fdb2e28000dcfdcfacda918d2c0e1812a683691bc0dab26878/ansible_pylibssh-1.3.0-cp311-cp311-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f720e6b1f50d99698e52cb6e41f05dfbb1f033a1d852f1ea5b1005055cf968e8", size = 2712675, upload-time = "2025-10-12T01:08:53.207Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/f5742f1ef3da34e067a29466aac88f179ae3ebadf02bfa4ce92e4b004918/ansible_pylibssh-1.3.0-cp311-cp311-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f613dfd50a1336019cb1a8a28098d3212fb163cc5e47da620c54ab0fe9af9a4e", size = 2188483, upload-time = "2025-10-12T01:08:54.946Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/3bec950215581bce7eb547256a3da053ad71fde72b31d11a76a86c64b52b/ansible_pylibssh-1.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:daeae1c936fc8145beb5843e365a314819b5ec0905500b795db945b64a3614ac", size = 2582419, upload-time = "2025-10-12T01:08:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/78b565e80b9354c4ff2f65a3e21ad7d6ce93d64197c531671d50988b214c/ansible_pylibssh-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0fadea3027fadd23feb5dbf6db3c2617ee69978e8521a6c47877947b4327c349", size = 2493133, upload-time = "2025-10-12T01:08:58.608Z" }, + { url = "https://files.pythonhosted.org/packages/42/93/095252bdd4961feba9b02591c0cf68036cc5cb7e517fa58cf4829f02a976/ansible_pylibssh-1.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7b38fb5fe94417b4f8348e499a8728a36599ec5c1b7c7ba79e424bf69794071", size = 2386366, upload-time = "2025-10-12T01:09:00.183Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/c6a1e3fb75c79b309ddb1e8ca79e00d5ca0ff10f3d46e66282f9d0085b79/ansible_pylibssh-1.3.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fab4f5e1b49782cd53765fc4bf239d92dc236550a9775de238d306452bf2cba", size = 2738851, upload-time = "2025-10-12T01:09:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4a/63d915984ff32d638ea6e2df390074d8f7f4e9efe34e27f96f392f50f087/ansible_pylibssh-1.3.0-cp312-cp312-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d1804c71ad5d7f5d03cc97447ed91ba1bdfde7fe72ba9d03478bb5220ff3d04", size = 2219773, upload-time = "2025-10-12T01:09:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/05/ec/de69b732b17e958f3d5b4c71010cec20cf30544d3a22b768dc712027ec46/ansible_pylibssh-1.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63859232d3a6d43a84ad8301c637a05be1ee47f0dee121015fc87bbacdd60532", size = 2609480, upload-time = "2025-10-12T01:09:05.477Z" }, ] [[package]] @@ -58,65 +41,77 @@ wheels = [ [[package]] name = "bcrypt" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, - { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, - { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, - { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, - { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, - { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, - { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, - { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, - { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, - { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, - { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, - { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, - { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, - { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, - { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367, upload-time = "2025-02-28T01:23:54.578Z" }, - { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644, upload-time = "2025-02-28T01:23:56.547Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881, upload-time = "2025-02-28T01:23:57.935Z" }, - { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203, upload-time = "2025-02-28T01:23:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" }, - { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" }, - { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, ] [[package]] name = "beaker-client" -version = "29.1" +version = "29.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beaker-common" }, @@ -127,29 +122,29 @@ dependencies = [ { name = "requests" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/3f/910d1ce1145eb81b73028f4eba41942dfe676aa5650fcd49d37c8fa37721/beaker-client-29.1.tar.gz", hash = "sha256:f114da2babb954c69a0e964e967d323b440df6be9c34efc5b199d268f98a16f7", size = 109167, upload-time = "2024-02-11T19:02:39.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/6dd0e32babd2da31a299f9ffef562a6e3b9fa32a1a97bcc56a57989a0ceb/beaker_client-29.2.tar.gz", hash = "sha256:fc00009287e630d6ace4d1fafddd01f5057c56de5d3e1b617d6b85d52f968579", size = 109232, upload-time = "2025-05-26T15:58:14.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/f0/77a36726e6253fb43ce3617a86950fc2b5639bb0f41ae0f0f84c8b2265ae/beaker_client-29.1-py3-none-any.whl", hash = "sha256:6bf8dbc410316b83a51c782619a82e118d5cb238aafa8e35335e2bdaf1657fd8", size = 168622, upload-time = "2024-02-11T19:02:37.628Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/14f6f5a388f4e4db41b45bf2ff3c666242284a661c41b5725a08072cf990/beaker_client-29.2-py3-none-any.whl", hash = "sha256:955b7a28b414818c450cdab6454a8dff99158e7b26ba2ab7f3bfb6809c948753", size = 168679, upload-time = "2025-05-26T15:58:11.761Z" }, ] [[package]] name = "beaker-common" -version = "29.1" +version = "29.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/be/88d376bf64220b77316b0ef639fcdd895fe36645e583d16238212b772480/beaker-common-29.1.tar.gz", hash = "sha256:83f62a9490c5d37ef736f356faef09738fe3ab0452bd2abfb273b38ba353f5b2", size = 33544, upload-time = "2024-02-11T19:02:41.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/5b/0f1658936b4482f421102b31064320988505c5d29025e85d618da87a484c/beaker_common-29.2.tar.gz", hash = "sha256:9fed5e2ed386f4114341a39d79b5681511ed49a4196c8df13edcc16ed203037c", size = 33637, upload-time = "2025-05-26T15:58:34.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/51/bb95c18f2f1b9cda380277f072e9eb7c6281404e91715093596893084fff/beaker_common-29.1-py3-none-any.whl", hash = "sha256:a86c226963e13c31e4540b83c314e619ec8266e85e26876902c367faea857beb", size = 43639, upload-time = "2024-02-11T19:02:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9d/6b61f1312077027ec904d3e1ab694d37b2b53eb6322e1b60ee17bd7781f5/beaker_common-29.2-py3-none-any.whl", hash = "sha256:bf064464d9734056ea3f9ec0226aa531d6c9f9b659be1826320a09399ce18050", size = 43682, upload-time = "2025-05-26T15:58:33.479Z" }, ] [[package]] name = "broker" -version = "0.7.3" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "click" }, { name = "dynaconf" }, - { name = "logzero" }, { name = "packaging" }, + { name = "python-json-logger" }, { name = "requests" }, { name = "rich" }, { name = "rich-click" }, @@ -189,6 +184,8 @@ dev = [ { name = "pytest" }, { name = "pytest-randomly" }, { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, ] docker = [ { name = "docker" }, @@ -232,7 +229,6 @@ requires-dist = [ { name = "hussh", marker = "extra == 'all'", specifier = ">=0.1.7" }, { name = "hussh", marker = "extra == 'hussh'", specifier = ">=0.1.7" }, { name = "hussh", marker = "extra == 'satlab'", specifier = ">=0.1.7" }, - { name = "logzero" }, { name = "openstacksdk", marker = "extra == 'all'" }, { name = "openstacksdk", marker = "extra == 'openstack'" }, { name = "packaging" }, @@ -250,6 +246,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-randomly", marker = "extra == 'all'" }, { name = "pytest-randomly", marker = "extra == 'dev'" }, + { name = "python-json-logger" }, { name = "requests" }, { name = "rich" }, { name = "rich-click" }, @@ -258,155 +255,228 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'" }, { name = "ssh2-python", marker = "extra == 'all'" }, { name = "ssh2-python", marker = "extra == 'ssh2-python'" }, + { name = "tox", marker = "extra == 'dev'" }, + { name = "tox-uv", marker = "extra == 'dev'" }, ] provides-extras = ["ansible-pylibssh", "ansibletower", "beaker", "dev", "docker", "hussh", "openstack", "paramiko", "podman", "ssh2-python", "satlab", "all"] +[[package]] +name = "cachetools" +version = "6.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/44/ca1675be2a83aeee1886ab745b28cda92093066590233cc501890eb8417a/cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6", size = 31571, upload-time = "2025-11-13T17:42:51.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503, upload-time = "2025-11-13T17:42:50.232Z" }, +] + [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.3.1" 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, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -420,47 +490,67 @@ wheels = [ [[package]] name = "cryptography" -version = "44.0.2" +version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807, upload-time = "2025-03-02T00:01:37.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361, upload-time = "2025-03-02T00:00:06.528Z" }, - { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350, upload-time = "2025-03-02T00:00:09.537Z" }, - { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572, upload-time = "2025-03-02T00:00:12.03Z" }, - { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124, upload-time = "2025-03-02T00:00:14.518Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122, upload-time = "2025-03-02T00:00:17.212Z" }, - { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831, upload-time = "2025-03-02T00:00:19.696Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583, upload-time = "2025-03-02T00:00:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753, upload-time = "2025-03-02T00:00:25.038Z" }, - { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550, upload-time = "2025-03-02T00:00:26.929Z" }, - { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367, upload-time = "2025-03-02T00:00:28.735Z" }, - { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843, upload-time = "2025-03-02T00:00:30.592Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057, upload-time = "2025-03-02T00:00:33.393Z" }, - { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789, upload-time = "2025-03-02T00:00:36.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919, upload-time = "2025-03-02T00:00:38.581Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812, upload-time = "2025-03-02T00:00:42.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571, upload-time = "2025-03-02T00:00:46.026Z" }, - { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832, upload-time = "2025-03-02T00:00:48.647Z" }, - { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719, upload-time = "2025-03-02T00:00:51.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852, upload-time = "2025-03-02T00:00:53.317Z" }, - { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906, upload-time = "2025-03-02T00:00:56.49Z" }, - { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572, upload-time = "2025-03-02T00:00:59.995Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631, upload-time = "2025-03-02T00:01:01.623Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792, upload-time = "2025-03-02T00:01:04.133Z" }, - { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" }, - { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886, upload-time = "2025-03-02T00:01:09.51Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387, upload-time = "2025-03-02T00:01:11.348Z" }, - { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922, upload-time = "2025-03-02T00:01:13.934Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715, upload-time = "2025-03-02T00:01:16.895Z" }, - { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876, upload-time = "2025-03-02T00:01:18.751Z" }, - { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719, upload-time = "2025-03-02T00:01:21.269Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513, upload-time = "2025-03-02T00:01:22.911Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432, upload-time = "2025-03-02T00:01:24.701Z" }, - { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421, upload-time = "2025-03-02T00:01:26.335Z" }, - { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081, upload-time = "2025-03-02T00:01:28.938Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] [[package]] @@ -474,11 +564,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] @@ -511,165 +601,178 @@ wheels = [ [[package]] name = "dynaconf" -version = "3.2.10" +version = "3.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a8/bd76f872481c783a7e29cf0d4eccf773b1a2418b971f389245964223dcd6/dynaconf-3.2.10.tar.gz", hash = "sha256:8dbeef31a2343c8342c9b679772c3d005b4801c587cf2f525f98f57ec2f607f1", size = 234228, upload-time = "2025-02-17T15:08:59.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/7a6f84b68268fe1d12e709faec7d293e0c37c9c03bacaf363de41e7e7568/dynaconf-3.2.12.tar.gz", hash = "sha256:29cea583b007d890e6031fa89c0ac489b631c73dbee83bcd5e6f97602c26354e", size = 313801, upload-time = "2025-10-10T19:54:06.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/02/3846e28288fa0ee0d45e3e01581629fbfcc1ca53121ad26e0f278daa8241/dynaconf-3.2.10-py2.py3-none-any.whl", hash = "sha256:7f70a4b8a8861efb88d8267aeb6f246c791dc34ecbb8299c26a19abd59113df6", size = 236203, upload-time = "2025-02-17T15:08:57.64Z" }, + { url = "https://files.pythonhosted.org/packages/26/68/51adede38ab2ee9ecfddb8b60a80a42b618a72f1018fcf60974e5d852831/dynaconf-3.2.12-py2.py3-none-any.whl", hash = "sha256:eb2a11865917dff8810c6098cd736b8f4d2f4e39ad914500e2dfbe064b82c499", size = 237788, upload-time = "2025-10-10T19:54:03.731Z" }, ] [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "filelock" -version = "3.18.0" +version = "3.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] [[package]] name = "gssapi" -version = "1.9.0" +version = "1.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/2f/fcffb772a00e658f608e657791484e3111a19a722b464e893fef35f35097/gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe", size = 94285, upload-time = "2024-10-03T06:13:02.484Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/47/aa7f24009de06c6a20f7eee2c4accfea615452875dc15c44e5dc3292722d/gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1", size = 708121, upload-time = "2024-10-03T06:12:19.526Z" }, - { url = "https://files.pythonhosted.org/packages/3a/79/54f11022e09d214b3c037f9fd0c91f0a876b225e884770ef81e7dfbe0903/gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6", size = 684749, upload-time = "2024-10-03T06:12:21.437Z" }, - { url = "https://files.pythonhosted.org/packages/18/8c/1ea407d8c60be3e3e3c1d07e7b2ef3c94666e89289b9267b0ca265d2b8aa/gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e", size = 778871, upload-time = "2024-10-03T06:12:23.395Z" }, - { url = "https://files.pythonhosted.org/packages/16/fd/5e073a430ced9babe0accde37c0a645124da475a617dfc741af1fff59e78/gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962", size = 870707, upload-time = "2024-10-03T06:12:25.434Z" }, - { url = "https://files.pythonhosted.org/packages/d1/14/39d320ac0c8c8ab05f4b48322d38aacb1572f7a51b2c5b908e51f141e367/gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560", size = 707912, upload-time = "2024-10-03T06:12:27.354Z" }, - { url = "https://files.pythonhosted.org/packages/cc/04/5d46c5b37b96f87a8efb320ab347e876db2493e1aedaa29068936b063097/gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd", size = 683779, upload-time = "2024-10-03T06:12:29.395Z" }, - { url = "https://files.pythonhosted.org/packages/05/29/b673b4ed994796e133e3e7eeec0d8991b7dcbed6b0b4bfc95ac0fe3871ff/gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe", size = 776532, upload-time = "2024-10-03T06:12:31.246Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/3bb8521da3ca89e202b50f8de46a9e8e793be7f24318a4f7aaaa022d15d1/gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f", size = 874225, upload-time = "2024-10-03T06:12:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/98/f1/76477c66aa9f2abc9ab53f936e9085402d6697db93834437e5ee651e5106/gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f", size = 698148, upload-time = "2024-10-03T06:12:34.545Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/b737e2a46efc63c6a6ad3baf0f3a8484d7698e673874b060a7d52abfa7b4/gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f", size = 681597, upload-time = "2024-10-03T06:12:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/4cbb8b6bc34ed02591e05af48bd4722facb99b10defc321e3b177114dbeb/gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec", size = 770295, upload-time = "2024-10-03T06:12:37.859Z" }, - { url = "https://files.pythonhosted.org/packages/c1/73/33a65e9d6c5ea43cdb1ee184b201678adaf3a7bbb4f7a1c7a80195c884ac/gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76", size = 867625, upload-time = "2024-10-03T06:12:39.518Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/6fbbeff852b6502e1d33858865822ab2e0efd84764caad1ce9e3ed182b53/gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c", size = 686934, upload-time = "2024-10-03T06:12:41.76Z" }, - { url = "https://files.pythonhosted.org/packages/c9/72/89eeb28a2cebe8ec3a560be79e89092913d6cf9dc68b32eb4774e8bac785/gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e", size = 672249, upload-time = "2024-10-03T06:12:43.7Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/3d9d4a198e34b844dc4acb25891e2405f8dca069a8f346f51127196436bc/gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28", size = 755372, upload-time = "2024-10-03T06:12:45.758Z" }, - { url = "https://files.pythonhosted.org/packages/67/00/f4be5211d5dd8e9ca551ded3071b1433880729006768123e1ee7b744b1d8/gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9", size = 845005, upload-time = "2024-10-03T06:12:47.885Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b7/bf/95eed332e3911e2b113ceef5e6b0da807b22e45dbf897d8371e83b0a4958/gssapi-1.10.1.tar.gz", hash = "sha256:7b54335dc9a3c55d564624fb6e25fcf9cfc0b80296a5c51e9c7cf9781c7d295b", size = 94262, upload-time = "2025-10-03T03:08:49.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/c2/0e25252f96f4213a666a32fdbfd5a287f115aec8bdb8a2e14af3ca392b7f/gssapi-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1456b60bbb999c7d2bf323c1ca9ee077dc6a59368737401c302c64bf0dd8a119", size = 669529, upload-time = "2025-10-03T03:08:10.212Z" }, + { url = "https://files.pythonhosted.org/packages/12/55/b948e8f104c99ef669ae939442651e9817e4584e9b056ff488138b6cd676/gssapi-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d75edd60f3105b362e4841ff86f358a9c6b9849e1327ea527b6a17b86e459207", size = 690220, upload-time = "2025-10-03T03:08:12.401Z" }, + { url = "https://files.pythonhosted.org/packages/f8/11/9779cbf496cff411bcce75379f7d7dc51d7831c93aa880d98e0b0e7be72f/gssapi-1.10.1-cp310-cp310-win32.whl", hash = "sha256:a589980c2c8c7ec7537b26b3d8d3becf26daf6f84a9534c54b3376220a9e82b5", size = 735551, upload-time = "2025-10-03T03:08:14.019Z" }, + { url = "https://files.pythonhosted.org/packages/da/f5/3d99ff06a12141cd02a751a5934a90c298b971fae0568965f55664567934/gssapi-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:77e92a1bdc4c72af4f1aa850787038741bd38e3fa6c52defee50125509539ffe", size = 820227, upload-time = "2025-10-03T03:08:15.837Z" }, + { url = "https://files.pythonhosted.org/packages/26/e4/d9d088d3dd7ab4009589af9d774d39e13de85709842210afa846efb02eb0/gssapi-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44be38aef1b26270dc23c43d8f124f13cf839cadcba63f5d011793eca2ec95f2", size = 675556, upload-time = "2025-10-03T03:08:17.743Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/ca520b74838edc98cdc3182821539a29da3cd2f00d94b70f860107d84a10/gssapi-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0be7195c96968df44f3cd2b79bbfa2ca3729d4bd91374947e93fde827bdab37f", size = 696622, upload-time = "2025-10-03T03:08:19.5Z" }, + { url = "https://files.pythonhosted.org/packages/bf/da/e7691856ebd762a09d4410fd6dcdb65aa7b09c258b70bf14a04d07ac69e2/gssapi-1.10.1-cp311-cp311-win32.whl", hash = "sha256:048736351b013290081472b2e523251246bc96d7ea74c97189d2af31f7d20bd6", size = 734716, upload-time = "2025-10-03T03:08:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/881178aac0bf010ca2608dd6b870e9b7c106ebee3203ddde202f45f934b1/gssapi-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:93166ed5d3ce53af721c2a9a115ffa645900f4b71c4810a18bff10f0a9843d0e", size = 823520, upload-time = "2025-10-03T03:08:22.942Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6f/b2dd133e3accf4be9106258331735b5d56959c018fb4b1952f70b35a3055/gssapi-1.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5c08ae5b5fa3faae1ad5bf9d4821a27da6974df0bf994066bf8e437ff101429", size = 672855, upload-time = "2025-10-03T03:08:24.649Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/6f499af7de07d1a3e7ad6af789a4a9b097d13b0342629bb152171bfee45f/gssapi-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec74a5e70241655b79c7de7dc750c58dae80482947973e019c67c8d53311981", size = 696430, upload-time = "2025-10-03T03:08:26.331Z" }, + { url = "https://files.pythonhosted.org/packages/20/81/4f70ad5ee531800fecbddd38870c16922d18cb9b5d4be2e1f4354a160f9b/gssapi-1.10.1-cp312-cp312-win32.whl", hash = "sha256:ed40213beec30115302bac3849134fbbfd5b0fdb60d8e4f2d9027cd44765f42b", size = 732078, upload-time = "2025-10-03T03:08:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/35/34/99ebc21b95765491af00d92b8332dba9ae5d357707ba81f05ba537acc4f8/gssapi-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:f0d5e5e6031e879d4050e0373cf854f5082ca234127b6553026a29c64ddf64ed", size = 826944, upload-time = "2025-10-03T03:08:29.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a9/39b5eefe1f7881d3021925c0a3183f1aa1a64d1cfe3ff6a5ab3253ddc2ef/gssapi-1.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:952c900ced1cafe7e7938052e24d01d4ba48f234a0ca7347c854c6d96f94ae26", size = 658891, upload-time = "2025-10-03T03:08:31.001Z" }, + { url = "https://files.pythonhosted.org/packages/15/09/9def6b103752da8e9d51a4258ffe2d4a97191e1067a1581324480b752471/gssapi-1.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df86f1dcc2a1c19c1771565661d05dd09cb1ce7ff2c3be261b3b5312458969f3", size = 682324, upload-time = "2025-10-03T03:08:32.685Z" }, + { url = "https://files.pythonhosted.org/packages/8b/24/615e0544dbf8bcb002d7f15bff44af502be99ed4ed2a64190779f47b0bc7/gssapi-1.10.1-cp313-cp313-win32.whl", hash = "sha256:37c2abb85e76d9e4bef967a752354aa6a365bb965eb18067f1f012aad0f7a446", size = 719627, upload-time = "2025-10-03T03:08:34.193Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/3c1c5dad78b193626a035661196dc3bed4d1544dd57e609fb6cc0e8838e5/gssapi-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:d821d37afd61c326ba729850c9836d84e5d38ad42acec21784fb22dd467345f4", size = 808059, upload-time = "2025-10-03T03:08:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/6c6bba3a06bc9e5c7fd7a8b4337c392b3074cbbce11525c94e8b7af856e9/gssapi-1.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a4d2aa439bcd08cd524a6e0c566137850e681b0fed62480aa765c097344387d7", size = 657421, upload-time = "2025-10-03T03:08:37.406Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/414e9cfa3c4f14682e40a5d61b8181936c78abf4aff0f1a91e9adaa20b5c/gssapi-1.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:86758d03906e10cb7feeedf26b5ead6661e844c54ef09d5e7de8e5ffb1154932", size = 685642, upload-time = "2025-10-03T03:08:39.115Z" }, + { url = "https://files.pythonhosted.org/packages/29/e4/812ef20519f020122b5207600fda2906a3d4fcc6536c8aeb764012c28470/gssapi-1.10.1-cp314-cp314-win32.whl", hash = "sha256:2ef6e30c37676fbb2f635467e560c9a5e7b3f49ee9536ecb363939efa81c82bc", size = 740154, upload-time = "2025-10-03T03:08:40.46Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/838a46df536111602d6582f8e8efecccaaf828b690c6305a2ef276c71e5e/gssapi-1.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:8f311cec5eabe0ce417908bcf50f60afa91a5b455884794eb02eb35a41d410c7", size = 826869, upload-time = "2025-10-03T03:08:42.524Z" }, ] [[package]] name = "hussh" -version = "0.1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/5b/c1ea916c7251f103e55a5e8261ad9efa47118d233ed9e8fa52b97e4eb808/hussh-0.1.8.tar.gz", hash = "sha256:3cd93070cc4d7865c17f3ec43f6cc62ff4fecc943b9acc13df798f6e96706494", size = 539069, upload-time = "2024-11-24T18:41:31.001Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/e3/7b71082b5c218a52f169844b0db1dc467687ae1540db585dcea96dd831fc/hussh-0.1.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27004d7aefbad8e239294a4ea20d2e1bc8f9470819b2d7b043e27be66e0df3a9", size = 1685895, upload-time = "2024-11-24T18:40:28.667Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c1/436851bdff6bf2d9bd25ad7dcf1a605495a1018dd8adffdc0234694607df/hussh-0.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c66c1d84507c57771a6912b308d89a87f4dde503dfa981b933263f09c5b27b70", size = 2012281, upload-time = "2024-11-24T18:40:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/a9/17/9644d2b7d4d78dbe274b3efe9cdd1f974614a05d3f7abfb3a918104539c8/hussh-0.1.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fec7fa9d90771a45633019c8dee02b8ed9f84665b6f940390db9471bd72442a3", size = 2190723, upload-time = "2024-11-24T18:39:12.32Z" }, - { url = "https://files.pythonhosted.org/packages/cf/cf/1c3b50df2bc869d2a54285953f1c7732d79b8f5c2bf5bc327d48b28c2157/hussh-0.1.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a4d2f0e6dd1ff0138ba4f12bf896244494da9e1bbf6b16b24594cf62a67938", size = 1578592, upload-time = "2024-11-24T18:39:25.638Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/a372c8884bca423ec14e63668fbb11d2c31c9fed3b10e0e60ff1ebae2007/hussh-0.1.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06d1a35986424fb52a8b8439801a6d0363163dcb25de17f6aaba4abad710d3f9", size = 1809684, upload-time = "2024-11-24T18:39:53.832Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/706976bcf3af6fe7c2e6ca58488c4d1fc6b1734d595204b67a4fe5bcf94a/hussh-0.1.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b9a86bce5a04f0982bcab3ebc656916c332851c7bfc140f57d6c1364b66cb56", size = 1834836, upload-time = "2024-11-24T18:39:42.324Z" }, - { url = "https://files.pythonhosted.org/packages/46/ac/136873409d83ea7ad562b36d980f8a6c0ececee89bac22a0dfc5ba4179a6/hussh-0.1.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3e29ea3e822f9c376e8b72c1d165bceef30282fb6464ecbc8b4ee2bcffec1f", size = 1903114, upload-time = "2024-11-24T18:40:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/6d/34/afa564119062020833fb3477dd8cdaa380dbbee3e99134aed8c3149c6c74/hussh-0.1.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49bb2fcd8eb7068e2391fdb46e03af25b5b0c94d48d95284773719b636ca03b9", size = 2485170, upload-time = "2024-11-24T18:40:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/bc3855a148b510f4818af8e156cf861c79ef5a43515ef425d7b03451cb0e/hussh-0.1.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c98adb29802e937d4aa5bf7bdd976e85bd68503358025106c626e250aebddb8", size = 1855224, upload-time = "2024-11-24T18:40:54.801Z" }, - { url = "https://files.pythonhosted.org/packages/7a/92/a48d85709cbfc2adb7adf8335072fb5b0330c9a68cf0f4c10d99f8548dcb/hussh-0.1.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:20d5309804eff07023db906e469d44a6e3e90c8104bbca437a19e2dd56948702", size = 1988955, upload-time = "2024-11-24T18:41:07.216Z" }, - { url = "https://files.pythonhosted.org/packages/d7/41/9a2245331623b490266e1e84ec229ea56ca76211ec36acda87325c7520bd/hussh-0.1.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a3089c8162f2817ae93766c0989c4f19db3d8d3f7a5471d22e0f8745e5c629f7", size = 2083614, upload-time = "2024-11-24T18:41:19.887Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c0/2cac21dd34a52471e83843aa7d04250b62c63ef39d36e351cdab68eaa470/hussh-0.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:df469618418045b455fae3c207c2eba6c1469abe1a6d901fadddf66fe0a86adf", size = 298869, upload-time = "2025-10-09T18:49:59.461Z" }, - { url = "https://files.pythonhosted.org/packages/a3/09/bc85b16f8a47d44e1740fa2480ed3cdc4c64a0c7f7e4c604b1ce7c797527/hussh-0.1.8-cp310-none-win_amd64.whl", hash = "sha256:e0c5f8db836d9d02da07919904cbfce5b30301c30360c5b1ff95bc3020b34a4c", size = 301302, upload-time = "2024-11-24T18:41:32.741Z" }, - { url = "https://files.pythonhosted.org/packages/fc/32/5c5dc7c5a775a15f42b1afdfb525beb52aa5058d6a7004d6d33433bd983a/hussh-0.1.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:578990a61a306eea409c2842f69527984359eb910044db06ce31ec7f032f6d6b", size = 1685895, upload-time = "2024-11-24T18:40:30.063Z" }, - { url = "https://files.pythonhosted.org/packages/bd/27/a31e124b188f2ccd0d372dc2fa325516d4d1a699914a9b959eecdca4b131/hussh-0.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1d0beec191a2f37245592bfd2c56a3b88229b904140905160cd3745f784d144d", size = 2012280, upload-time = "2024-11-24T18:40:18.621Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/2dde813bc5bf9659e6d55e2893920a0e2ac406bb26569b4aae86d4ccdc80/hussh-0.1.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f41782d4061f33ca04f5dccfe1dedfe82917b4fd1a51c066cb7485fabc610dd", size = 2190724, upload-time = "2024-11-24T18:39:14.452Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c9/bb3a29279ddcdeb2e14d27b5e3259ee6f719c91d67f1f89908224c3b868a/hussh-0.1.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6bec839c8e0e56427fe70edac9ed4e06f4ae6404c5d377e4f86be96eb90ca3cc", size = 1578592, upload-time = "2024-11-24T18:39:27.478Z" }, - { url = "https://files.pythonhosted.org/packages/7a/81/8706850455c59c015942710b1f19b9cc13c8b6afab882da8a5be3217c4a6/hussh-0.1.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68809516886dbf5828b4ea79af3b016ec8e33a65d1b3a94f4629a92a356291c9", size = 1809680, upload-time = "2024-11-24T18:39:55.245Z" }, - { url = "https://files.pythonhosted.org/packages/e0/83/0cf81533e8edc58f45619bafb712ebd48e76dd969be3520bc888239f3e52/hussh-0.1.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34677ac89e9c45fe2e06598602e6710f0b35b35c8c260b46ee249e2362c319fa", size = 1834836, upload-time = "2024-11-24T18:39:43.659Z" }, - { url = "https://files.pythonhosted.org/packages/a8/16/90e99c4cc900755975dd8037137926e9ecd94ae9e16c603acbba3d5cc1ff/hussh-0.1.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e0c818b8e97b027a9da4c9ef32354411e3fe516a3ddf9a21eb4df28f90b49b2", size = 1903113, upload-time = "2024-11-24T18:40:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/2e/9712ad61058246c856ed870fb54d77fb21851db3479051f170c1a8a5daef/hussh-0.1.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a139af353519689bc3aaf7ae7029768f50f3af181e687311112fd568690378c", size = 2485173, upload-time = "2024-11-24T18:40:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/04/da/bf8a2bb440c46c3270cf2f524789b01966ad1db8087695e54a6a59c3c64d/hussh-0.1.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:78e4cdde8016d9d1b94880d62328ea86ea822ec8d1ba9952018c7bb36a73ae63", size = 1855224, upload-time = "2024-11-24T18:40:56.81Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8f/4c5660aa1c2f4f24d87865852369ec101d55af07817b700af0e36d303388/hussh-0.1.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ddbcabefa45a42dd02c4cf1ed7721c7499d5792d1fc9acf2c8dcd88c82998af8", size = 1988959, upload-time = "2024-11-24T18:41:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9f/5fd237245d57d8eecff6c0b36f72426c69e11e5019207bb2f91ed661967c/hussh-0.1.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:feefcbfa1e7a70854022de93172f30cfb33bae9e2fd3a63b68206ca229504b8f", size = 2083619, upload-time = "2024-11-24T18:41:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a9/7f8c720a9949b27209744a3c7041e197cff3fe831170abc295bca62543b9/hussh-0.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:5f64715fb33e793feb6a89e11d82f97fd16c19202cf224a403fdd220be7faf34", size = 298730, upload-time = "2025-10-09T18:50:00.787Z" }, - { url = "https://files.pythonhosted.org/packages/aa/58/b869653669eabb7286d255bc6ca1724a7c1140827175df4f049f9eeeaa50/hussh-0.1.8-cp311-none-win_amd64.whl", hash = "sha256:fe76f52f3144f7017592268cabddf5199ad18462b37eb87ffb71af96f0e1efe5", size = 301283, upload-time = "2024-11-24T18:41:33.962Z" }, - { url = "https://files.pythonhosted.org/packages/34/23/ba38efc6eda2fd07e8d165f8c6f1ae7d6c1702813f7f2054e31316767ac1/hussh-0.1.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d315285860942330e413e2a29debc3eab2c74f81af8a69b722230c22bcb48df7", size = 1685897, upload-time = "2024-11-24T18:40:31.3Z" }, - { url = "https://files.pythonhosted.org/packages/8b/8f/5d64216228fc9afee984df07c5092640648fb9c6acf80a1927d88b7e4e27/hussh-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:657ee4ad4546ce4454987fd68189cbad3b399e362aa7302b9d59bb0698d9c088", size = 2012277, upload-time = "2024-11-24T18:40:19.869Z" }, - { url = "https://files.pythonhosted.org/packages/96/b5/cce77848759f444273f6154c6f9dd1b97cab058908318ec9f13c692735d8/hussh-0.1.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c6f6dbb9c174dc97254079be87dbe182221ec37fc17be9da3805d647669afc2", size = 2190725, upload-time = "2024-11-24T18:39:15.703Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7c/dc999d64c1f9e5bd9cf55f95b2af74e055b81c3c733a93ac23014f5d1b9f/hussh-0.1.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a496bbb0c73d2fe2faac7c810129eb820978b950525af464a4efe3c65785b2e", size = 1578592, upload-time = "2024-11-24T18:39:28.669Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/eba76000d487dbae866603c31ba8a52fc82d568f324b98134d87cc652f54/hussh-0.1.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55d9f3b5ccfefd57519fd68496677bbb9f321caef37f8ae3934bd952357c192a", size = 1809682, upload-time = "2024-11-24T18:39:56.54Z" }, - { url = "https://files.pythonhosted.org/packages/ba/db/ef487c788b06129fab74afaba5f1a0e2e4a17183fb4787b4448ffca2f0b4/hussh-0.1.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b107a4007eb32d1f0a09aeed5b3f7ff1cee89a19f512800604ffba9a4036080", size = 1834836, upload-time = "2024-11-24T18:39:45.649Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fd/237a114a5cb50d8256d132bf2aa362a5af6b559c962fd9e708193c7aa224/hussh-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28c843f63d882199ce595f518d1199589534bc1e24e9cafe1b69a8663f695bcd", size = 1903114, upload-time = "2024-11-24T18:40:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/2d/db/0ad902719535a07bc12c89d05a59fface995d841ab5799c4d77c8d08762e/hussh-0.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e30f7f23a84c36a93ee645cf4989e9b2443727e311478b62af1eeccc99dc2d88", size = 2485173, upload-time = "2024-11-24T18:40:42.92Z" }, - { url = "https://files.pythonhosted.org/packages/cc/aa/84664359c5cf055a8e995d6552d37eef4fc26d67110c4baa9417de526b19/hussh-0.1.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:30156e87eec435f1f8629ca2391d77d0611f40229f46f516f918ccd86d109a1b", size = 1855226, upload-time = "2024-11-24T18:40:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/16/70/27d78dd222eedbfcd561f03eae70965c1288f2de26bd784bee978b4f02fe/hussh-0.1.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d013dd74313ad31e5be12a80a9ff84b54db911558e284b94f9236ec6e141a4df", size = 1988959, upload-time = "2024-11-24T18:41:10.593Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5f/8cb51c0531ff925729d2535b51f5a9bc8c757b45a7a89979ffd13f307efb/hussh-0.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34cb571ece99be062ba1ba191ac266bc075a13439aaa5cf369cd6a42399ca884", size = 2083618, upload-time = "2024-11-24T18:41:22.506Z" }, - { url = "https://files.pythonhosted.org/packages/55/76/dfb30a0b4be2758b307fc0fb9ba9a63200bae3e14827cc10ae93128fb907/hussh-0.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:66e560d5c99deb57d4ac23b05406fe32987885024bc67639aadf4974c4ffb01c", size = 299257, upload-time = "2025-10-09T18:50:02.055Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/e53656932ec7ea6c2e65166f2a1f5f65309536820b890d573ab216260a02/hussh-0.1.8-cp312-none-win_amd64.whl", hash = "sha256:c2fb37edd8af33bc290fd68d9b0b145bb1163d6e33fd07c3e874dcabce263600", size = 301732, upload-time = "2024-11-24T18:41:35.091Z" }, - { url = "https://files.pythonhosted.org/packages/29/73/d4372d381762a938af674c1e4f4af4274a821b78c75a1479639d744c19d4/hussh-0.1.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:bbdee97273b1cbb5fb4ffd0f5ee441f261d6893effa946fd96f80b910415e677", size = 1685897, upload-time = "2024-11-24T18:40:33.172Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5d/37ea76886b56bb1a055d7316699ff6c8c4229af5a50b0edcecd01141e310/hussh-0.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1caf522ecb17b73005f489539fb0aaef1e189b944e12f417f48aa4759aaf439c", size = 2012278, upload-time = "2024-11-24T18:40:21.158Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f5/3a5fe98924965c564ec0ec25dbd06db5a55e264e184e615df9b367a74fb8/hussh-0.1.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:766ec7ff140290d7e5abd5b75bcf2205d893f113dd2e4b4ee01fb10f510a0d83", size = 2190724, upload-time = "2024-11-24T18:39:17.994Z" }, - { url = "https://files.pythonhosted.org/packages/21/bf/cf71977ac08b2d1b33392617030356c5b51eca89cec2579e96b96905ea41/hussh-0.1.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6514b4c8b8383917a0e3b3ed87e553ec14046ea093034c0b209eec84d1dc599", size = 1578592, upload-time = "2024-11-24T18:39:29.975Z" }, - { url = "https://files.pythonhosted.org/packages/9d/11/bab146ac3dc6712bf3c7500b0f9f3f69ab67ddadb70178ff014dab4702ae/hussh-0.1.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1beecc7889cfccba973ccbbec18d98c84e5d1580d58a71e19c319769cc449dca", size = 1809681, upload-time = "2024-11-24T18:39:57.823Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/0c871c6dac0910617d568f3bb3a8a779854ad3ef7743a1532a7e646b3971/hussh-0.1.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0da8442868fab9936ae19061d45ad986fcef5142d14e81291b4355f0930b06d", size = 1834836, upload-time = "2024-11-24T18:39:46.872Z" }, - { url = "https://files.pythonhosted.org/packages/e5/02/8ede42200df3bf7113a10b2ecd19d748f31f5f737f6daff0e820930ba4d0/hussh-0.1.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba72ec9c83b663aada238e402a375ede7182e6adfb48af8556e401b0dc66dee7", size = 1903115, upload-time = "2024-11-24T18:40:09.752Z" }, - { url = "https://files.pythonhosted.org/packages/cc/44/581946789eeac42225c38a11769a4aede7471723c8ce2a196a3290c73de6/hussh-0.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2faa27c4226bbb8acb4eb3edbe96d2250f5fb7290df79198a9e48fbf341a338", size = 2485173, upload-time = "2024-11-24T18:40:44.356Z" }, - { url = "https://files.pythonhosted.org/packages/11/d4/e0bd9ba6bf38092321782483f8a41b69268afc07f87e72e7ea14ac5cd3e4/hussh-0.1.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e027478b6d43499fed8fd02a3df02f2b34cd5867320f458a6e6b775afd78c364", size = 1855226, upload-time = "2024-11-24T18:40:59.459Z" }, - { url = "https://files.pythonhosted.org/packages/d7/20/81e2d9d026cf3fffd299120966d3e4561bd32d5a7985969be243f312d4eb/hussh-0.1.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4fb9945b04db3ee6732e879cd6b99eb377dcfca8a9cc75aa4f1ea54ef4bb693", size = 1988959, upload-time = "2024-11-24T18:41:12.189Z" }, - { url = "https://files.pythonhosted.org/packages/b0/95/f2e302e4f3ee43a92155ae295a8e4fc4f0f2476de0f65be9a4a9a4b8035b/hussh-0.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:05064854da17f2eb000109f6446db07d97d41d7ec029b9e39804b371ddea01d0", size = 2083617, upload-time = "2024-11-24T18:41:23.825Z" }, - { url = "https://files.pythonhosted.org/packages/69/6c/1d95b23ee2f4e60541e9c966859b391824c16a050f15bfe7731fcb54d77d/hussh-0.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:801d4bb5f0a1ae8720c664f9076ef0ef967a70e03aeab870d00be9e66edd4529", size = 298875, upload-time = "2025-10-09T18:50:03.751Z" }, - { url = "https://files.pythonhosted.org/packages/40/42/fc7276e55ccfc83c1560d84a2dda05136d83a2cde201cf2f3ca877b24ab9/hussh-0.1.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a2cc7f99712b999d7fd9d06b4af7eceb1116d368b3cf650ac88ffc16a1588537", size = 1793996, upload-time = "2025-10-09T18:49:45.604Z" }, - { url = "https://files.pythonhosted.org/packages/57/b1/e2fb20dd3abb2992996d8f08da0d8600009ce56d5f18cb67c7f6dc5d58e9/hussh-0.1.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:872b68e0e6e18118c3adadb88f7ba9cce2b877ce8bc23606884a85e43a54531c", size = 2107207, upload-time = "2025-10-09T18:49:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/cd/01/e4a95917f66df9dce1cd45f24357d246438d2816db5c1dbadb339cef40a1/hussh-0.1.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef3aaa84524f2b25d57ecbc8478c668288054ea1095e72de30d7be5bcd67ac1c", size = 2320715, upload-time = "2025-10-09T18:49:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/93/ce/a244c83b5bc9694a6fbc932a4f5abb574da17b8cdabe50c5749ed82f89d1/hussh-0.1.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436437c13d0f860f3069581c3d9449ea4d213c5492c35d6e5434cc75e7aa2d17", size = 1699017, upload-time = "2025-10-09T18:49:30.284Z" }, - { url = "https://files.pythonhosted.org/packages/9a/fb/aca90405b13afda49fa7be68c73edcd01001f7c3ea667180caa5feba97e0/hussh-0.1.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cf642a887162468b5046dcc00f88820360b938e73094ea8a513e2d843cd5a4c", size = 1970817, upload-time = "2025-10-09T18:49:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/d8/51/c12a920b16ec05383b04baaa29b9441d63fc2371da4a31aaa16dce8b503f/hussh-0.1.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07bf00dcc5c6de49f76408921cb2b4fa9f023d1f22d56e98dd7c72fba878e0a8", size = 1994970, upload-time = "2025-10-09T18:49:33.327Z" }, - { url = "https://files.pythonhosted.org/packages/56/7b/d30f773c0cf917509b59e473c1c320414d410ab2387d7c18f1c7f5572164/hussh-0.1.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e6acbfefd05f1b09ea18049476df37bb8fd08ff5c09c72c2323828b5c5ad9", size = 2028698, upload-time = "2025-10-09T18:49:39.797Z" }, - { url = "https://files.pythonhosted.org/packages/76/7f/e53d67a826ca431859905f1d02fba716a74a76eed781aa75cc800cd77ce9/hussh-0.1.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c2006303c32eb1e7c62cb4de7ee2cfaebecb27f7806d3b5ab668c3fcec78b18", size = 2635037, upload-time = "2025-10-09T18:49:48.42Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/6236a1f3bdbeec4abeffc7464a3082bdbd9ae7694d39fe3820633d662e6d/hussh-0.1.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:179c0ed8d3881ea5f05d22988709f1a1f1326fe27b4bb7f3577ed24bab27c023", size = 1987803, upload-time = "2025-10-09T18:49:51.283Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0c/9b635ee49df35418def55b2af913df24642aab00d993dbd7f67ffd313800/hussh-0.1.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:93e14895930cfd5c67385782056700a375aae8208829d644eb75869d136dfa96", size = 2152146, upload-time = "2025-10-09T18:49:54.134Z" }, - { url = "https://files.pythonhosted.org/packages/76/72/224d8fa732ca38cf9ec4aad9245e7f55f2c442f09de4b6a318492f9c8b50/hussh-0.1.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:34d2c06f5931c9a48f126f116dad91b4c1e28e5cad347c36c2dd673cb773c979", size = 2253749, upload-time = "2025-10-09T18:49:57.221Z" }, - { url = "https://files.pythonhosted.org/packages/a9/40/ebb296aaabfa19189e0b7c7f1c4980f7c296f15a627fae0e7509091b4491/hussh-0.1.8-cp314-cp314-win_amd64.whl", hash = "sha256:5501542ff1edeab865114231e1ecc53dfea0fad5c8baaaadccd9da8e79444da5", size = 299137, upload-time = "2025-10-09T18:50:05.079Z" }, - { url = "https://files.pythonhosted.org/packages/91/5f/99a62c6382fe07e5e1edf9c4a4f5ffa389e7e7249697a4712ef167441a3f/hussh-0.1.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:10ed4bc62eb65b54816df213c97f0f912389e46757bbb8a9bea616a2fde2f721", size = 1685908, upload-time = "2024-11-24T18:40:37.402Z" }, - { url = "https://files.pythonhosted.org/packages/04/66/c4280d443898ba9feaeb8019a51dfc745e2b8bbc4b18ec1885a6ef6a3cb5/hussh-0.1.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa99f173a5cbcf93ef6c5e35131002f399cb0db6c8a7c82ca5ed5da8c6b06adb", size = 2012292, upload-time = "2024-11-24T18:40:25.723Z" }, - { url = "https://files.pythonhosted.org/packages/e0/73/bbee4c8b94c3139d3bb4d43e80266029164c6930b4f438de56dfa8b046a0/hussh-0.1.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce679b48efe07047678284aefc76e1ae73ecdad210ded4c33d8fa98e49c54132", size = 2190732, upload-time = "2024-11-24T18:39:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/0a2ce4ae469122fa2356e97d608807fb7fcc0ab246bcaeabf1a77df0f1c8/hussh-0.1.8-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba350759d4cf91bb88528baf1f710f11e9f6aa5196ecb7563bc50dc4d451eceb", size = 1578585, upload-time = "2024-11-24T18:39:37.887Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ca/ead8413d815d71a13b54d9ff7049348dc2df5f93e3840473ca30095ad900/hussh-0.1.8-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e821d613e35d4ab5953136f2b5a5848951d1cfc45f7f3050b73306b455786c1", size = 1808439, upload-time = "2024-11-24T18:40:02.431Z" }, - { url = "https://files.pythonhosted.org/packages/96/78/24ea81a6bf801d6be51f9f816e5fe70c010d9fd7af15a94e39046ab737bb/hussh-0.1.8-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:504c5d92779c37e0ffa8322904e5c4413b336a90efabf2e387fe008269203d4d", size = 1834829, upload-time = "2024-11-24T18:39:51.356Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3b/0f270588f51bd54346f5aff8f9c9f5e31440cb411b701f6e3daad27e4cd4/hussh-0.1.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a95e14599b88599dd2e169a35742ca1e45cc476cbbc0fc5c1e3973163e94c0", size = 1902542, upload-time = "2024-11-24T18:40:13.598Z" }, - { url = "https://files.pythonhosted.org/packages/87/73/146a3039fd649c4a60830c5605cddea55e9211b3e214013e1d329e6c198f/hussh-0.1.8-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:979d065ea62ecb458f7a274206b75904b0287b3bebe5b95eb23c98936fff66dc", size = 2485179, upload-time = "2024-11-24T18:40:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/e9/8f/6a3801e93a698b0d6606fa153803737d06a9a37e7a2370328c355eb7baa6/hussh-0.1.8-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:39b6319ae5196610394e70e9a493d5518afc3abc4ca58742cc61f39b683d23c9", size = 1855215, upload-time = "2024-11-24T18:41:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5e/7cbec99aeb72471ace8c947d9629683007c3211956426cd500c5f2f4e476/hussh-0.1.8-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e124e04ea643534be79e3ca067cc92d7c132a2eed3bccdfc95d5bd057848d46c", size = 1988964, upload-time = "2024-11-24T18:41:16.354Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a4/0857cbd3b38b6d4f97653f2a9aa2d21e255dbebd874100851725f042793d/hussh-0.1.8-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5236690cad74b41bfb8dbc9bbd7650f2a2179870de43d2e8c4325fdaa21a574e", size = 2083625, upload-time = "2024-11-24T18:41:27.74Z" }, +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/b5/74892f02525ea30b78d60e77d4e7c43b01408c7df4578825c30ec3251a25/hussh-0.2.0.tar.gz", hash = "sha256:17dd0ee91b20ab1b3ac8f25de725df38052e186ce97264b80997c6ca2ddabfac", size = 619909, upload-time = "2025-11-23T05:51:55.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/d7/f73fce30fd8d69dfcdfa46133ef49a179dcd9d24e801052a60166b32d90e/hussh-0.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:162d8e253fed84a1032be6fd5b6add39aeba66ee41bfad253944cda399577a88", size = 3272579, upload-time = "2025-11-23T05:50:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/2ff93606103a64abf930c808dd2ca908bc9a117792b8c9597f68ff089caf/hussh-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f389288cd73722efdd32f5953cd541e296caf0685ad00a01e31c207da2885691", size = 3502298, upload-time = "2025-11-23T05:50:18.073Z" }, + { url = "https://files.pythonhosted.org/packages/7e/21/fa79cdef27cc25e464c999e483b36a00c76359d4cb62ed2d5e23828d2508/hussh-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd86a0f15668bf26d85c52ff86888d89a176e06b50f2d506aebc92dc38300691", size = 3806994, upload-time = "2025-11-23T05:48:53.724Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/db27cc27b6d92c4b2b1d0c17d827c5d5090ec3923409ec524a49df0b32a7/hussh-0.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64be108d0fa3dfcb0211c91e414fefd64d7902ab8ea82da3cf658aed7c53600a", size = 3390065, upload-time = "2025-11-23T05:49:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/a8/04/86d6125beed686a3b298ce0120cd841bfd53b71405a33686477ef6c96173/hussh-0.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41b33ca1cea68ef5d3d4b721871bf4414851909ea7011784a7e15b722695874", size = 3683661, upload-time = "2025-11-23T05:49:44.803Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d3/3739ba372f40df93a8632434c3a691397f0e66bd874328e2009f0b645f46/hussh-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d00cd40bab16d539d01343769b7f558774bceb29f481a764de1f5f42b3a5a0a2", size = 3555085, upload-time = "2025-11-23T05:49:27.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/ef38f851e4df168e57b14709f2986d2a1882d27de5648fc99ef7d1e00a72/hussh-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:befd8a4557dea920d9aec302bda50d76caa06947702a12c06c4d5e56c4bc4583", size = 3566286, upload-time = "2025-11-23T05:50:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/82/20/b8607b0172f25c0d3cf388152e2a299638dc7c66105a85eb895fb1b7cec3/hussh-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9508749644263d3ce6acdfb7299f35262c2ee2e8fa705d9663b7973deb54c126", size = 4149726, upload-time = "2025-11-23T05:50:50.291Z" }, + { url = "https://files.pythonhosted.org/packages/52/a3/c19a43be45a684daa7d77eb0870068ad4f3460858e193f1bb6b279e64557/hussh-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43d2b8e2df90846d3064e3b9ef3e3c81f9cdb6a73216d4d8f45801d304454aef", size = 3686373, upload-time = "2025-11-23T05:51:06.601Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8b/ff75ed41b73a8626ce3d709e108ac364aeabbc44113e37638e84bb052e50/hussh-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cfa24f66c8385c877a921fa9efe19b89b7463e65d76d66844d24093695a63c12", size = 3836044, upload-time = "2025-11-23T05:51:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/35/a5/86cd06cbb81741c3e877793efcd030387795a9017d683cd4fbaf849c8d3c/hussh-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2a4486f1e1eac035c7099a992d6180119708668b2e17f237cf473331fa85e0ba", size = 3801992, upload-time = "2025-11-23T05:51:39.017Z" }, + { url = "https://files.pythonhosted.org/packages/04/52/bdd83524847a78cc91caacdf0392f34a0b9265200c95693a3c0dcdfb4c68/hussh-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:069828d4b58109aead9b594eeb07382e41045f0ce763290337f7296d9d957393", size = 1835097, upload-time = "2025-11-23T05:51:56.567Z" }, + { url = "https://files.pythonhosted.org/packages/49/bc/d146d496c100910e7c694069bbf1184ce11557c26e969e50338a3646ab7e/hussh-0.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:18b2bb0520fc7979d2d850e25891b055906f97e34279946785902abe493e1917", size = 3272087, upload-time = "2025-11-23T05:50:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/0d/33/73956125b478ac40e2d1ca4d4a620415ece06359f47acdd1e08f00857d5f/hussh-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8371ffb7a3294d2f381a043160c69111cfa0132d4da1992ceccaffede4b513d5", size = 3502534, upload-time = "2025-11-23T05:50:19.882Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fc/70558c801f7e0a1d8585a7d628cdf800f25d4c2de5a1148026142989678d/hussh-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5d3e5c4383e7c77621d66fef773f07e0f9883ee6dd279e97a6e508d5509fbd", size = 3806951, upload-time = "2025-11-23T05:48:55.883Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/bb59f038c7e730dce09941c60d841d6d2b996520a80bad92918d87130cb9/hussh-0.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f40889dac117fb2ca1a278a6cb19238de92bd17bbae2d9b7acf14b0da75fdee", size = 3390209, upload-time = "2025-11-23T05:49:12.374Z" }, + { url = "https://files.pythonhosted.org/packages/95/2b/6d4e84f2456281cdc3f9bec3e9d47e049879b8102b3464afe8644719a2f4/hussh-0.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acf6abdb3481c69499e497de46f37b391aaa87983bbd9def91dc284869b18f47", size = 3685018, upload-time = "2025-11-23T05:49:46.769Z" }, + { url = "https://files.pythonhosted.org/packages/05/90/2292d76159ea952541fdfebebb3d215aab2bf704284969ccc5b38fe95303/hussh-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a46440327912ca086acfacbeb2ce860d9bcd30fb41abd3545080b172eade022d", size = 3554945, upload-time = "2025-11-23T05:49:29.2Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/666d611968a42c4a2d1e946eb3259ef946915a21b1b15c678c43dd01c48b/hussh-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c7daaec3fb53b27fb7932fe903ecf2c3f6cbb6300e2a204bf289edbf96b837", size = 3566528, upload-time = "2025-11-23T05:50:03.34Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ec/92c88218b27a494b50a965bbedbb91ea402cc56d23c466bb6dca17b573c8/hussh-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64961b55aa9cf38a386a3a425451c689de5ec84382f54eaf7b6cdcad2278072", size = 4151126, upload-time = "2025-11-23T05:50:51.973Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f6/8409b6ef8936cb21f3bc9fb793b8ad06df876e9acfee636f50fd61642aa9/hussh-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:60bcc4bc685a2bb535a96e0c65177ee70b438905b1a2ed7988724522ad420bad", size = 3687026, upload-time = "2025-11-23T05:51:08.434Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/313305b1bb5eefd79746b30b69d609c759118db05a491f48a3735b74e012/hussh-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f44088d1d12a312c018182f73c21dd3c714646c51dde03296895049deb806d8", size = 3838222, upload-time = "2025-11-23T05:51:24.677Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0a/93a39fe967c4182d3abcae1a4e55a2558859758fc642a7f56b55c401363e/hussh-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8cfd79e7d041a14de69dbcb958c27ceed5c81a4da218a21f52b9e9e28c5fedc3", size = 3802154, upload-time = "2025-11-23T05:51:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5f/93481deac24cc13f0c6cca627e75fd5c017f4e12da1c2cfc456e4211d19e/hussh-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:e320cf3ecbee79acd42f4d88a8d7e44aa1cd9737fc97fe5cfbc2155450f3c52f", size = 1835003, upload-time = "2025-11-23T05:51:58.34Z" }, + { url = "https://files.pythonhosted.org/packages/db/82/8501cfac5c947ba68cb13d506cae29d18615127e9de7fe65bc1fe7808a35/hussh-0.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4256c98f93fc1decd350586cfa843c0b561c1b3e18d8fc131e7d99d394a92658", size = 3271501, upload-time = "2025-11-23T05:50:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/62e92a6f42d13b88a757423d2dc8df63a7be1971b9cc7f9647f98164a768/hussh-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:479f739a2bad2b4277740923981778958b96322898d54c5f00fd6f8647ab33e3", size = 3501752, upload-time = "2025-11-23T05:50:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d6/388bae09bee5526dd8e5d4051f3e0d0e28c1399f057dab1dbe5d9aa5837f/hussh-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd681436628af44efe70ffe1cf7878dfb5a2a5e138d979ed82df4e91dc4e26cf", size = 3808367, upload-time = "2025-11-23T05:48:57.739Z" }, + { url = "https://files.pythonhosted.org/packages/89/37/c210b1a2885442e9085d834236032e49e6c0ccc024225315973bb266e821/hussh-0.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3bbc6d870e37c8d423fa5536344ba4b150a73a21b9542be0bea2751c2b810d81", size = 3397170, upload-time = "2025-11-23T05:49:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/51/5e/9865f6f842503c5e6e56f30fa4021fb27d6d859f8f7e697363c2bf75b0b5/hussh-0.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d581d1b20ec103af1c019960d94f0c57e1e23eb8e9a97d2666c9bb5ecf3722", size = 3683840, upload-time = "2025-11-23T05:49:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/aa/22/c3190cced05f9829b78ea2edb0794c1d2ecfd1e5f87d5efb217c927840c1/hussh-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb272d1144dda9707c2384a3c0da1529289e32ebde7220c9f80ddb3488e434f1", size = 3557021, upload-time = "2025-11-23T05:49:30.809Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f6/526e61e1d52b58528bedc54d6b705f5af4bfbd4282c465b355db109b29df/hussh-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d51dd07d5bcbe87bb29405bd34b8295e6dd90e2b2d1231cb441a78abab0ddfcb", size = 3568944, upload-time = "2025-11-23T05:50:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b8/8d850e6e27d48d8dc979456ce82cb71dea990c88898e9f99e0305b89b9e8/hussh-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2205b7920b5cb9ea058f3cf77ff72961d8c79ae540e7d1ccad7db72f0a5c382c", size = 4151277, upload-time = "2025-11-23T05:50:53.577Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/e749119c6ba60279d9ab0486a6354ac4aa5a2098dc71640365a3f9768b37/hussh-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:91825c7ca567a60505967b6eb41b8aa1b6f772ce8062450cc1f6cc26707a4a47", size = 3693206, upload-time = "2025-11-23T05:51:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/2f/73/e26f5d8d5b0c31afac66164af67a82ff910d9791de53e2ac51ca3c6d7a6e/hussh-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:545011ec49aa1180f746503785aa15b5359037a597d8e8187e33db561c119553", size = 3834936, upload-time = "2025-11-23T05:51:26.627Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/9c85387377125ec9d7a01f7e78857e592953f4b5f27b2730bb83fb196dfd/hussh-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b498e40c7b1b700a8ccefd0f0fc318937e2f09d61efcac5a93b7f6aae71f37e", size = 3805260, upload-time = "2025-11-23T05:51:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/0de98c2d186d56089a61c78d981a4502e85cdf0aee4719c8f02662b0890f/hussh-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8d29d74337d5e7b2c3d56bf22a91da7b49cfc008e365b0cff6002f12e8a08998", size = 1841836, upload-time = "2025-11-23T05:52:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6d/f202930c318d2e477faa6db17403b708b1c2e6aebd3309715f055fc0a87d/hussh-0.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ae49fa031ea82c05ebdaa378fca081e2dc04282ccf857754524600d157d5659d", size = 3271098, upload-time = "2025-11-23T05:50:39.508Z" }, + { url = "https://files.pythonhosted.org/packages/62/1f/1e090ef8a3976cf4866c96e2a8d9f52fb9553e7ac047d7bc5d79ddb5c342/hussh-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e8a0eab028d5aaa11ce523d1273584fa7872f994465d7e9bf23d7a3106db3258", size = 3501602, upload-time = "2025-11-23T05:50:23.554Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a6/f2484239c48672754feaddc348a1fc5542b539a0c7b4018ae677dfc3fe5a/hussh-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:062fee2677d6542872e72304504c298e0f2db5eb86dbcebb8ab76a24a11a0514", size = 3807478, upload-time = "2025-11-23T05:48:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/8215e24ab91f779cbd16fb094792c7c145c02f61847dfc7a2a81e916a928/hussh-0.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8dcd6741c73ecb89e55567e07bec1d85c0912c24c7b088c2fde568824ba743f8", size = 3398270, upload-time = "2025-11-23T05:49:16.117Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d7/6c37a0ebc0bbe9ad96905f5fa28c2bbe11ae8576aec235e513f466df30d3/hussh-0.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:691183a476c944b0cc94537fc35f87fc026846276c7a9e3d4c66a11bf7aeb4ae", size = 3683866, upload-time = "2025-11-23T05:49:50.426Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a4/58b4574627be0a7102de84bd2a1042232d1f95383d9dc7f3980c71f56398/hussh-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df64cf8f41e3c3ffdd3fe8311578177d05bc190e98489c50f8a50e02ecd547", size = 3556756, upload-time = "2025-11-23T05:49:33.193Z" }, + { url = "https://files.pythonhosted.org/packages/0d/93/f22e663d07d6038528f55a72a850e2b19f188909344e26b782b92daa63fe/hussh-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07814604555c4d9394679d8977ef4b028e39f9abfe1ade6100ad5fc4c0ba0fd0", size = 3568618, upload-time = "2025-11-23T05:50:07.361Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7c/12bfc150ec47c6cbc955f797d5087c1d0a4509887d2c6098367d074061a0/hussh-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8ee6e6eccf5e17564ca6357fc989f00fb67c106c95e6019c7d2221581c721214", size = 4151007, upload-time = "2025-11-23T05:50:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f3/13920a58c5088a8d94121b6a1dcf306f8dd2892d01cb3fc3addc77653f92/hussh-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e6485ea9bc21541595a25ee2d06d7504f09b3274ceecc695cbbb4440ab00f4d9", size = 3693751, upload-time = "2025-11-23T05:51:12.471Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0a/3a11067e086e62a0dc00446e0108538378364fe7c46bc59a77f18a31ce8e/hussh-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fa5febd3a61e81d18162afd1ef18361c85a519887f44ea562114aa4d703db394", size = 3834197, upload-time = "2025-11-23T05:51:28.669Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/c342b24ba7d56e1a289488d236342cb975b88fe96b38c1f5b8bfe82d2b7c/hussh-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78137bd9620fea6396fa10d9d77e3354b5a95513dd4bb6d7d1b94789c64741ea", size = 3805058, upload-time = "2025-11-23T05:51:44.75Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/a21ce6ec589fe8354f4ea0a74f57dde9cc4ce96a3b6b4ac1977d9cc07276/hussh-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ac76c4f4f0f84bd65b9bb032d0887e98859195917cbfb59c949c9b660370286", size = 1840552, upload-time = "2025-11-23T05:52:01.865Z" }, + { url = "https://files.pythonhosted.org/packages/91/8b/ce510e7e52249f636444dcbdc287e0981f683cdf57fba9199ee9ab35465e/hussh-0.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e5ccb129d2a278395f7e381952686f384519b2df5d787fa1c335d9112ac27251", size = 3270859, upload-time = "2025-11-23T05:50:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/38/4c/55f5fdadafddc4c57af5f7124dfe14506b44257e40ba308d13c46437f002/hussh-0.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b21d8d47e960bad91dad1dbb26deee5f140c14c47a995eb9fc267f3b2cf9339", size = 3498048, upload-time = "2025-11-23T05:50:25.28Z" }, + { url = "https://files.pythonhosted.org/packages/a6/43/fa0f1705f9c11dcc7cdefa5dcb37c8548c922b62e08eb76ef331529f8dc7/hussh-0.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67b32470500da1dfc3c27c3b2a840fb0a4e5d11e4461cb9fa98d7891d68123b", size = 3809045, upload-time = "2025-11-23T05:49:01.373Z" }, + { url = "https://files.pythonhosted.org/packages/14/f1/8d77965f35557e09084bb1290422a5609bc549b900e603cf601d4faf339a/hussh-0.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f4b66af8ef0504185f2f169f556efb12dbd536ff742c1d7c797237ad9d27bc4", size = 3393956, upload-time = "2025-11-23T05:49:17.839Z" }, + { url = "https://files.pythonhosted.org/packages/01/e9/e699202223936d7fea05bb2723565c7a265096661d1ebccdb4fa4463a679/hussh-0.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4db847d32e9be8601aadd97c601e1a1e003417e8ad09d7877d55d052c34c674d", size = 3685033, upload-time = "2025-11-23T05:49:52.093Z" }, + { url = "https://files.pythonhosted.org/packages/64/23/abda50cf23f8d1b90e7283d4fe1b8831953a3f7725c0b901185612a7a141/hussh-0.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76ecc4c649e4abda4bec68f472581f7ae9c5d3b658c20057c39c0ae8ce91c3d4", size = 3555323, upload-time = "2025-11-23T05:49:35.193Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/2cb7f6e78d4b80b3ad9a586b8b46d35554b1e19bc3280c067ebf859d72d6/hussh-0.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72d3298c3e0dfb9b47cecc542792b24898fde7afd22f66c4ace9467f258805fc", size = 3568152, upload-time = "2025-11-23T05:50:09.005Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/07ac1391a7219b793ff01dc45d2052afcf4ec32d9d91c67ef74f5d18a9f3/hussh-0.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:40aec938ea4a113aabba49bd280520642f2a750fb5cdbfe04266085cde8a4fad", size = 4152497, upload-time = "2025-11-23T05:50:57.672Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/3b8d0b9fd5d26899fede2446be2481fb5e2bd5d373f4de0f1c2e30329f86/hussh-0.2.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30ce138316fea7084fdfa2653f748ad342b21c262d3addbe835bcf83a8c0305d", size = 3692188, upload-time = "2025-11-23T05:51:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/1da0004e7022d6a7734b7ad83f9197e7ab9bc952a2cd0eae9d5e0b859c93/hussh-0.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fcd7d8339586e5a843c81bf11b8077e4d0fc683b255848a822f79362e0b5b2e", size = 3836072, upload-time = "2025-11-23T05:51:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/d8/81/c1028d78149bdc0c37a5fa12a846706c294505258deb913c1c4c8a854078/hussh-0.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7b35563f0c7027ce8f3d6000eb575db54b6f4b297fb37265a936cfed7e0c6385", size = 3805201, upload-time = "2025-11-23T05:51:46.494Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/961df74129d9b4404611ae234f8332b838a9326c7147d3dedacfead06930/hussh-0.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:b39b6819088595b257f4675032e19cbbf739b9ed0477afe867b4574fd85299d1", size = 1841996, upload-time = "2025-11-23T05:52:04.807Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/f67f00f16b563c1d8a01a3f42cae7bc799a5a7ae3f085cae6e2cda8fd74c/hussh-0.2.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8fea4555d9669cecf40f09a6642e2c5adab7d602b473782f4b33b4435401a5ac", size = 3270189, upload-time = "2025-11-23T05:50:46.494Z" }, + { url = "https://files.pythonhosted.org/packages/ea/38/63bef3051b5787aa3fec6676b4992d9f3cf3191ee3b18780a040b7fce574/hussh-0.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9849bdf5171d3276d5f5f80b1296e030e20a77ed63f90378c1ff4c1955ea2895", size = 3505290, upload-time = "2025-11-23T05:50:30.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/f5/0c80a463e56ba0e063d1c41e5e3931b86c6726fb1b5d3371e5f9d1976be1/hussh-0.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fd80e5e080a2dbb4c9a2b52da8c80e5be9f8f742d49f9e2ffb5d126f9f0db6", size = 3812284, upload-time = "2025-11-23T05:49:07.01Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a3/04a7e7291488043d74677bae5d045ebcdc43deafe4264dfc6906de844b22/hussh-0.2.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81d5300878d78510e19ed08200ce0f159ad655426e94aa6e54fd1787b0cd905b", size = 3391559, upload-time = "2025-11-23T05:49:23.583Z" }, + { url = "https://files.pythonhosted.org/packages/04/fe/3e320303a21e80e2db322570b4690bf3886d86eb98bdf6b84cbcac4836e7/hussh-0.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b27aa1c38d0da4df29949cd8dff70fe576e5601cbe39edeb09fb07ac00476", size = 3689558, upload-time = "2025-11-23T05:49:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/b6269f3821a0130e90432df8f4858b7b905c38ced540d2d1eac72aedc744/hussh-0.2.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f4a17dec47a0e676934f21540b7fab7e67ac753fc3e9467e5989959243bae04", size = 3555181, upload-time = "2025-11-23T05:49:41.182Z" }, + { url = "https://files.pythonhosted.org/packages/78/0f/f0fb3654d8db7406472adf82ffb858c2ac956a23f8f9846f3fc7df706dfe/hussh-0.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b7fadc75b16f5df6716153b27a4d43711f59621701a63ddc921df2278027211", size = 3566283, upload-time = "2025-11-23T05:50:14.217Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/30c1e5ddca951ed99beb3a482d3dbfacb0a02c2656783fe48e29c6baaba2/hussh-0.2.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f84710cc27cb001b304d41a84c81829466c76517169b047f4a2acbd259e19be6", size = 4156141, upload-time = "2025-11-23T05:51:03.249Z" }, + { url = "https://files.pythonhosted.org/packages/28/2c/80e6cde928843f1992a17bdbf85ad73515e16bd894c8bfc644cfdc56af48/hussh-0.2.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:d5675733d9bacadd8ef1ad6bcdc4bc9a2503e722436fa94d6a47138ced16b98e", size = 3687329, upload-time = "2025-11-23T05:51:19.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/7e/d752bf24625be2b6c3d118419d0f1d0c664d6a36ef38cba5cb53f111ebd2/hussh-0.2.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:32e34b4e8ec8888d77f2151ce745eb43549546fbaf45305345e7562e4f5154ee", size = 3841016, upload-time = "2025-11-23T05:51:35.491Z" }, + { url = "https://files.pythonhosted.org/packages/0c/76/1db23be2c4aa03056454b4e2a6d0a09d0ed2d17859f182991360b16c1155/hussh-0.2.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f4dc0bef4bcabedc09cea7adb9cd9bbffae762b889eac96d8c49942261e7996b", size = 3802179, upload-time = "2025-11-23T05:51:51.825Z" }, ] [[package]] name = "identify" -version = "2.6.10" +version = "2.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" 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, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "invoke" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, ] [[package]] @@ -740,168 +843,225 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/8a/803a45dc660770ac7e2d74fc1260a15ade29d2234120854747491b4a7a02/keystoneauth1-5.12.0-py3-none-any.whl", hash = "sha256:2e514b03615e2d9162f0c07c823a61a636e6d4df38ff4a34b7511a04e8a4166a", size = 343402, upload-time = "2025-08-21T09:34:09.38Z" }, ] -[[package]] -name = "logzero" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/9a/018883ee64df0900bde1ac314868f81d12cbc450e51b216ab55e6e4dfc7d/logzero-1.7.0.tar.gz", hash = "sha256:7f73ddd3ae393457236f081ffebd044a3aa2e423a47ae6ddb5179ab90d0ad082", size = 577803, upload-time = "2021-03-17T11:24:21.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/68/aa714515d65090fcbcc9a1f3debd5a644b14aad11e59238f42f00bd4b298/logzero-1.7.0-py2.py3-none-any.whl", hash = "sha256:23eb1f717a2736f9ab91ca0d43160fd2c996ad49ae6bad34652d47aba908769d", size = 16162, upload-time = "2021-03-17T11:24:19.849Z" }, -] - [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/1f/a3b6b74a451ceb84b471caa75c934d2430a4d84395d38ef201d539f38cd1/lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c", size = 8076838, upload-time = "2025-04-23T01:44:29.325Z" }, - { url = "https://files.pythonhosted.org/packages/36/af/a567a55b3e47135b4d1f05a1118c24529104c003f95851374b3748139dc1/lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7", size = 4381827, upload-time = "2025-04-23T01:44:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/50/ba/4ee47d24c675932b3eb5b6de77d0f623c2db6dc466e7a1f199792c5e3e3a/lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf", size = 5204098, upload-time = "2025-04-23T01:44:35.809Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0f/b4db6dfebfefe3abafe360f42a3d471881687fd449a0b86b70f1f2683438/lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28", size = 4930261, upload-time = "2025-04-23T01:44:38.271Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/0bb1bae1ce056910f8db81c6aba80fec0e46c98d77c0f59298c70cd362a3/lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609", size = 5529621, upload-time = "2025-04-23T01:44:40.921Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/e7b66a533fc4a1e7fa63dd22a1ab2ec4d10319b909211181e1ab3e539295/lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4", size = 4983231, upload-time = "2025-04-23T01:44:43.871Z" }, - { url = "https://files.pythonhosted.org/packages/11/39/a38244b669c2d95a6a101a84d3c85ba921fea827e9e5483e93168bf1ccb2/lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7", size = 5084279, upload-time = "2025-04-23T01:44:46.632Z" }, - { url = "https://files.pythonhosted.org/packages/db/64/48cac242347a09a07740d6cee7b7fd4663d5c1abd65f2e3c60420e231b27/lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f", size = 4927405, upload-time = "2025-04-23T01:44:49.843Z" }, - { url = "https://files.pythonhosted.org/packages/98/89/97442835fbb01d80b72374f9594fe44f01817d203fa056e9906128a5d896/lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997", size = 5550169, upload-time = "2025-04-23T01:44:52.791Z" }, - { url = "https://files.pythonhosted.org/packages/f1/97/164ca398ee654eb21f29c6b582685c6c6b9d62d5213abc9b8380278e9c0a/lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c", size = 5062691, upload-time = "2025-04-23T01:44:56.108Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bc/712b96823d7feb53482d2e4f59c090fb18ec7b0d0b476f353b3085893cda/lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b", size = 5133503, upload-time = "2025-04-23T01:44:59.222Z" }, - { url = "https://files.pythonhosted.org/packages/d4/55/a62a39e8f9da2a8b6002603475e3c57c870cd9c95fd4b94d4d9ac9036055/lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b", size = 4999346, upload-time = "2025-04-23T01:45:02.088Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/a393728ae001b92bb1a9e095e570bf71ec7f7fbae7688a4792222e56e5b9/lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563", size = 5627139, upload-time = "2025-04-23T01:45:04.582Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/9dcaaad037c3e642a7ea64b479aa082968de46dd67a8293c541742b6c9db/lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5", size = 5465609, upload-time = "2025-04-23T01:45:07.649Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/ebcae89edf27e61c45023005171d0ba95cb414ee41c045ae4caf1b8487fd/lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776", size = 5192285, upload-time = "2025-04-23T01:45:10.456Z" }, - { url = "https://files.pythonhosted.org/packages/42/ad/cc8140ca99add7d85c92db8b2354638ed6d5cc0e917b21d36039cb15a238/lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7", size = 3477507, upload-time = "2025-04-23T01:45:12.474Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/597ce090da1097d2aabd2f9ef42187a6c9c8546d67c419ce61b88b336c85/lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250", size = 3805104, upload-time = "2025-04-23T01:45:15.104Z" }, - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" }, - { url = "https://files.pythonhosted.org/packages/be/7c/8c3f15df2ca534589717bfd19d1e3482167801caedfa4d90a575facf68a6/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37", size = 4208552, upload-time = "2025-04-23T01:49:29.949Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/9567afb1665f64d73fc54eb904e418d1138d7f011ed00647121b4dd60b38/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571", size = 4331091, upload-time = "2025-04-23T01:49:32.842Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ab/fdbbd91d8d82bf1a723ba88ec3e3d76c022b53c391b0c13cad441cdb8f9e/lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4", size = 3487862, upload-time = "2025-04-23T01:49:36.296Z" }, +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, ] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.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, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } 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, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -949,15 +1109,15 @@ wheels = [ [[package]] name = "os-service-types" -version = "1.8.1" +version = "1.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pbr" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/da/66eaa235e053eb2451464ec131487dec01b5259105688e9f6771d07d45fe/os_service_types-1.8.1.tar.gz", hash = "sha256:c3d60134ee509cf55452c73ff8bd41891bcb6cf42421a159c0138824e126402b", size = 27348, upload-time = "2025-10-30T10:01:03.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/86/9fc6f238d5a14a90ac8dacb051072aab694e91de76d4b8be56fd4dba4cf4/os_service_types-1.8.1-py3-none-any.whl", hash = "sha256:348e029248fccf063a117d30ac603d1afa396fda436bdbfa71f90ae920f13511", size = 24734, upload-time = "2025-10-30T10:01:02.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/26/0937af7b4383f1eba5bca789b8d191c0e09e59bb64962b18f4a14534ce41/os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e", size = 24876, upload-time = "2025-11-21T13:55:46.093Z" }, ] [[package]] @@ -971,16 +1131,17 @@ wheels = [ [[package]] name = "paramiko" -version = "3.5.1" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, { name = "cryptography" }, + { name = "invoke" }, { name = "pynacl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" }, + { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" }, ] [[package]] @@ -1009,39 +1170,39 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.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, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "podman" -version = "5.4.0.1" +version = "5.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/5c/ff701e438360a526a728eae3c18ee439c0875e4b937947f1cd04497ae17e/podman-5.4.0.1.tar.gz", hash = "sha256:ee537aaa44ba530fad7cd939d886a7632f9f7018064e7831e8cb614c54cb1789", size = 68926, upload-time = "2025-02-19T16:59:52.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/36/070e7bf682ac0868450584df79198c178323e80f73b8fb9b6fec8bde0a65/podman-5.6.0.tar.gz", hash = "sha256:cc5f7aa9562e30f992fc170a48da970a7132be60d8a2e2941e6c17bd0a0b35c9", size = 72832, upload-time = "2025-09-05T09:42:40.071Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/38/6d4416f3ec8dc18149847bbb31cc54af20de063844029b00fb0c0d4c533c/podman-5.4.0.1-py3-none-any.whl", hash = "sha256:abd32e49a66bf18a680d9a0ac3989a3f4a3cc7293bc2a5060653276d8ee712f4", size = 84516, upload-time = "2025-02-19T16:59:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/8c62f05b104d9f00edbb4c298b152deceb393ea67f0288d89d1139d7a859/podman-5.6.0-py3-none-any.whl", hash = "sha256:967ff8ad8c6b851bc5da1a9410973882d80e235a9410b7d1e931ce0c3324fbe3", size = 88713, upload-time = "2025-09-05T09:42:38.405Z" }, ] [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1050,21 +1211,21 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, ] [[package]] name = "prettytable" -version = "3.16.0" +version = "3.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/b1/85e18ac92afd08c533603e3393977b6bc1443043115a47bb094f3b98f94f/prettytable-3.16.0.tar.gz", hash = "sha256:3c64b31719d961bf69c9a7e03d0c1e477320906a98da63952bc6698d6164ff57", size = 66276, upload-time = "2025-03-24T19:39:04.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c7/5613524e606ea1688b3bdbf48aa64bafb6d0a4ac3750274c43b6158a390f/prettytable-3.16.0-py3-none-any.whl", hash = "sha256:b5eccfabb82222f5aa46b798ff02a8452cf530a352c31bddfa29be41242863aa", size = 33863, upload-time = "2025-03-24T19:39:02.359Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, ] [[package]] @@ -1104,45 +1265,75 @@ wheels = [ [[package]] name = "pycparser" -version = "2.22" +version = "2.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" 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, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pynacl" -version = "1.5.0" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/46/aeca065d227e2265125aea590c9c47fbf5786128c9400ee0eb7c88931f06/pynacl-1.6.1.tar.gz", hash = "sha256:8d361dac0309f2b6ad33b349a56cd163c98430d409fa503b10b70b3ad66eaa1d", size = 3506616, upload-time = "2025-11-10T16:02:13.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d6/4b2dca33ed512de8f54e5c6074aa06eaeb225bfbcd9b16f33a414389d6bd/pynacl-1.6.1-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:7d7c09749450c385301a3c20dca967a525152ae4608c0a096fe8464bfc3df93d", size = 389109, upload-time = "2025-11-10T16:01:28.79Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/e8dbb8ff4fa2559bbbb2187ba0d0d7faf728d17cb8396ecf4a898b22d3da/pynacl-1.6.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc734c1696ffd49b40f7c1779c89ba908157c57345cf626be2e0719488a076d3", size = 808254, upload-time = "2025-11-10T16:01:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/44/f9/f5449c652f31da00249638dbab065ad4969c635119094b79b17c3a4da2ab/pynacl-1.6.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3cd787ec1f5c155dc8ecf39b1333cfef41415dc96d392f1ce288b4fe970df489", size = 1407365, upload-time = "2025-11-10T16:01:40.454Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2f/9aa5605f473b712065c0a193ebf4ad4725d7a245533f0cd7e5dcdbc78f35/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b35d93ab2df03ecb3aa506be0d3c73609a51449ae0855c2e89c7ed44abde40b", size = 843842, upload-time = "2025-11-10T16:01:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/32/8d/748f0f6956e207453da8f5f21a70885fbbb2e060d5c9d78e0a4a06781451/pynacl-1.6.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dece79aecbb8f4640a1adbb81e4aa3bfb0e98e99834884a80eb3f33c7c30e708", size = 1445559, upload-time = "2025-11-10T16:01:33.663Z" }, + { url = "https://files.pythonhosted.org/packages/78/d0/2387f0dcb0e9816f38373999e48db4728ed724d31accdd4e737473319d35/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c2228054f04bf32d558fb89bb99f163a8197d5a9bf4efa13069a7fa8d4b93fc3", size = 825791, upload-time = "2025-11-10T16:01:34.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/ef6fb7eb072aaf15f280bc66f26ab97e7fc9efa50fb1927683013ef47473/pynacl-1.6.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:2b12f1b97346f177affcdfdc78875ff42637cb40dcf79484a97dae3448083a78", size = 1410843, upload-time = "2025-11-10T16:01:36.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fb/23824a017526850ee7d8a1cc4cd1e3e5082800522c10832edbbca8619537/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e735c3a1bdfde3834503baf1a6d74d4a143920281cb724ba29fb84c9f49b9c48", size = 801140, upload-time = "2025-11-10T16:01:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d1/ebc6b182cb98603a35635b727d62f094bc201bf610f97a3bb6357fe688d2/pynacl-1.6.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3384a454adf5d716a9fadcb5eb2e3e72cd49302d1374a60edc531c9957a9b014", size = 1371966, upload-time = "2025-11-10T16:01:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/64/f4/c9d7b6f02924b1f31db546c7bd2a83a2421c6b4a8e6a2e53425c9f2802e0/pynacl-1.6.1-cp314-cp314t-win32.whl", hash = "sha256:d8615ee34d01c8e0ab3f302dcdd7b32e2bcf698ba5f4809e7cc407c8cdea7717", size = 230482, upload-time = "2025-11-10T16:01:47.688Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/942477957fba22da7bf99131850e5ebdff66623418ab48964e78a7a8293e/pynacl-1.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5f5b35c1a266f8a9ad22525049280a600b19edd1f785bccd01ae838437dcf935", size = 243232, upload-time = "2025-11-10T16:01:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/bdbc0d04a53b96a765ab03aa2cf9a76ad8653d70bf1665459b9a0dedaa1c/pynacl-1.6.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d984c91fe3494793b2a1fb1e91429539c6c28e9ec8209d26d25041ec599ccf63", size = 187907, upload-time = "2025-11-10T16:01:46.328Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/3cfb3b4f3519f6ff62bf71bf1722547644bcfb1b05b8fdbdc300249ba113/pynacl-1.6.1-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:a6f9fd6d6639b1e81115c7f8ff16b8dedba1e8098d2756275d63d208b0e32021", size = 387591, upload-time = "2025-11-10T16:01:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/18/21/b8a6563637799f617a3960f659513eccb3fcc655d5fc2be6e9dc6416826f/pynacl-1.6.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e49a3f3d0da9f79c1bec2aa013261ab9fa651c7da045d376bd306cf7c1792993", size = 798866, upload-time = "2025-11-10T16:01:55.688Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6c/dc38033bc3ea461e05ae8f15a81e0e67ab9a01861d352ae971c99de23e7c/pynacl-1.6.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7713f8977b5d25f54a811ec9efa2738ac592e846dd6e8a4d3f7578346a841078", size = 1398001, upload-time = "2025-11-10T16:01:57.101Z" }, + { url = "https://files.pythonhosted.org/packages/9f/05/3ec0796a9917100a62c5073b20c4bce7bf0fea49e99b7906d1699cc7b61b/pynacl-1.6.1-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3becafc1ee2e5ea7f9abc642f56b82dcf5be69b961e782a96ea52b55d8a9fc", size = 834024, upload-time = "2025-11-10T16:01:50.228Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b7/ae9982be0f344f58d9c64a1c25d1f0125c79201634efe3c87305ac7cb3e3/pynacl-1.6.1-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ce50d19f1566c391fedc8dc2f2f5be265ae214112ebe55315e41d1f36a7f0a9", size = 1436766, upload-time = "2025-11-10T16:01:51.886Z" }, + { url = "https://files.pythonhosted.org/packages/b4/51/b2ccbf89cf3025a02e044dd68a365cad593ebf70f532299f2c047d2b7714/pynacl-1.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:543f869140f67d42b9b8d47f922552d7a967e6c116aad028c9bfc5f3f3b3a7b7", size = 817275, upload-time = "2025-11-10T16:01:53.351Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/dd9ee8214edf63ac563b08a9b30f98d116942b621d39a751ac3256694536/pynacl-1.6.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a2bb472458c7ca959aeeff8401b8efef329b0fc44a89d3775cffe8fad3398ad8", size = 1401891, upload-time = "2025-11-10T16:01:54.587Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c1/97d3e1c83772d78ee1db3053fd674bc6c524afbace2bfe8d419fd55d7ed1/pynacl-1.6.1-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3206fa98737fdc66d59b8782cecc3d37d30aeec4593d1c8c145825a345bba0f0", size = 772291, upload-time = "2025-11-10T16:01:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ca/691ff2fe12f3bb3e43e8e8df4b806f6384593d427f635104d337b8e00291/pynacl-1.6.1-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:53543b4f3d8acb344f75fd4d49f75e6572fce139f4bfb4815a9282296ff9f4c0", size = 1370839, upload-time = "2025-11-10T16:01:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/06fe5389d30391fce006442246062cc35773c84fbcad0209fbbf5e173734/pynacl-1.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:319de653ef84c4f04e045eb250e6101d23132372b0a61a7acf91bac0fda8e58c", size = 791371, upload-time = "2025-11-10T16:02:01.075Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7a/e2bde8c9d39074a5aa046c7d7953401608d1f16f71e237f4bef3fb9d7e49/pynacl-1.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:262a8de6bba4aee8a66f5edf62c214b06647461c9b6b641f8cd0cb1e3b3196fe", size = 1363031, upload-time = "2025-11-10T16:02:02.656Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b6/63fd77264dae1087770a1bb414bc604470f58fbc21d83822fc9c76248076/pynacl-1.6.1-cp38-abi3-win32.whl", hash = "sha256:9fd1a4eb03caf8a2fe27b515a998d26923adb9ddb68db78e35ca2875a3830dde", size = 226585, upload-time = "2025-11-10T16:02:07.116Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/b419180f3fdb72ab4d45e1d88580761c267c7ca6eda9a20dcbcba254efe6/pynacl-1.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a569a4069a7855f963940040f35e87d8bc084cb2d6347428d5ad20550a0a1a21", size = 238923, upload-time = "2025-11-10T16:02:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/35/76/c34426d532e4dce7ff36e4d92cb20f4cbbd94b619964b93d24e8f5b5510f/pynacl-1.6.1-cp38-abi3-win_arm64.whl", hash = "sha256:5953e8b8cfadb10889a6e7bd0f53041a745d1b3d30111386a1bb37af171e6daf", size = 183970, upload-time = "2025-11-10T16:02:05.786Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, - { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, - { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, - { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1150,91 +1341,124 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, ] [[package]] name = "pytest-randomly" -version = "3.16.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367, upload-time = "2024-10-25T15:45:34.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396, upload-time = "2024-10-25T15:45:32.78Z" }, + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, ] [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240, upload-time = "2025-03-17T00:55:46.783Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854, upload-time = "2025-03-17T00:55:48.783Z" }, - { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963, upload-time = "2025-03-17T00:55:50.969Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, - { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1242,9 +1466,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -1258,120 +1482,135 @@ wheels = [ [[package]] name = "rich" -version = "14.0.0" +version = "14.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] [[package]] name = "rich-click" -version = "1.8.8" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/7a/4b78c5997f2a799a8c5c07f3b2145bbcda40115c4d35c76fbadd418a3c89/rich_click-1.8.8.tar.gz", hash = "sha256:547c618dea916620af05d4a6456da797fbde904c97901f44d2f32f89d85d6c84", size = 39066, upload-time = "2025-03-09T23:20:31.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d8/f2c1b7e9a645ba40f756d7a5b195fc104729bc6b19061ba3ab385f342931/rich_click-1.9.4.tar.gz", hash = "sha256:af73dc68e85f3bebb80ce302a642b9fe3b65f3df0ceb42eb9a27c467c1b678c8", size = 73632, upload-time = "2025-10-25T01:08:49.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/69/963f0bf44a654f6465bdb66fb5a91051b0d7af9f742b5bd7202607165036/rich_click-1.8.8-py3-none-any.whl", hash = "sha256:205aabd5a98e64ab2c105dee9e368be27480ba004c7dfa2accd0ed44f9f1550e", size = 35747, upload-time = "2025-03-09T23:20:29.831Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6a/1f03adcb3cc7beb6f63aecc21565e9d515ccee653187fc4619cd0b42713b/rich_click-1.9.4-py3-none-any.whl", hash = "sha256:d70f39938bcecaf5543e8750828cbea94ef51853f7d0e174cda1e10543767389", size = 70245, upload-time = "2025-10-25T01:08:47.939Z" }, ] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" }, ] [[package]] name = "ruamel-yaml-clib" -version = "0.2.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301, upload-time = "2024-10-20T10:12:35.876Z" }, - { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728, upload-time = "2024-10-20T10:12:37.858Z" }, - { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230, upload-time = "2024-10-20T10:12:39.457Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712, upload-time = "2024-10-20T10:12:41.119Z" }, - { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936, upload-time = "2024-10-21T11:26:37.419Z" }, - { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580, upload-time = "2024-10-21T11:26:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393, upload-time = "2024-12-11T19:58:13.873Z" }, - { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326, upload-time = "2024-10-20T10:12:42.967Z" }, - { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079, upload-time = "2024-10-20T10:12:44.117Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, - { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload-time = "2024-10-20T10:12:55.657Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload-time = "2024-10-20T10:12:57.155Z" }, - { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload-time = "2024-10-20T10:12:58.501Z" }, - { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload-time = "2024-10-20T10:13:00.211Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload-time = "2024-10-21T11:26:46.038Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload-time = "2024-10-21T11:26:47.487Z" }, - { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload-time = "2024-12-11T19:58:17.252Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload-time = "2024-10-20T10:13:01.395Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload-time = "2024-10-20T10:13:02.768Z" }, - { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload-time = "2024-10-20T10:13:04.377Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload-time = "2024-10-20T10:13:05.906Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload-time = "2024-10-20T10:13:07.26Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload-time = "2024-10-20T10:13:08.504Z" }, - { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload-time = "2024-10-21T11:26:48.866Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload-time = "2024-10-21T11:26:50.213Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload-time = "2024-12-11T19:58:18.846Z" }, - { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload-time = "2024-10-20T10:13:09.658Z" }, - { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" }, +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, + { url = "https://files.pythonhosted.org/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, + { url = "https://files.pythonhosted.org/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, ] [[package]] name = "ruff" -version = "0.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" }, - { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" }, - { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" }, - { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" }, - { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" }, - { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" }, +version = "0.14.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, + { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, + { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, + { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, + { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, + { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] [[package]] name = "setuptools" -version = "80.1.0" +version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/b2/bd26ed086b842b68c8fe9aac380ad7e5118cf84fa7abd45bb059a88368a8/setuptools-80.1.0.tar.gz", hash = "sha256:2e308396e1d83de287ada2c2fd6e64286008fe6aca5008e0b6a8cb0e2c86eedd", size = 1354038, upload-time = "2025-04-30T17:41:06.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/f6/126c9309c8fe93e5d6bb850593cd58d591daf2da45cc78b61e48d8d95879/setuptools-80.1.0-py3-none-any.whl", hash = "sha256:ea0e7655c05b74819f82e76e11a85b31779fee7c4969e82f72bab0664e8317e4", size = 1240689, upload-time = "2025-04-30T17:41:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] @@ -1385,114 +1624,192 @@ wheels = [ [[package]] name = "ssh2-python" -version = "1.1.2.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/14/7b1072447ffa15d1e1b099611ecd8e985f2dfa62b2ad436dee303a141f6a/ssh2_python-1.1.2.post1.tar.gz", hash = "sha256:c39b9f11fd0e2a7422480aa61b8787c444b26a402907c9e2798767763763bb7c", size = 2132633, upload-time = "2025-01-23T03:19:03.681Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/76/b395cca37dde897bc1e0ab7aadc98de181ace9ac19aaf236d6148c05ba03/ssh2_python-1.1.2.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa9a4633bfe3e59bb6ae9c491c1fae22ee409807ad253b06b190ab7be43466f", size = 4890980, upload-time = "2025-01-23T03:18:40.195Z" }, - { url = "https://files.pythonhosted.org/packages/aa/71/470cd75e2aaa56a06107f1d1079d2fb572dadf4156200b8fd4fe832e43f0/ssh2_python-1.1.2.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffb00c332571a1d643f47efcad46e4559adf30ff76e0d8cad7fa4f50e01bf31e", size = 4538004, upload-time = "2025-01-23T03:18:54.402Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/b3b9473020f99f3df0e5e0e92cef8abef08a273493eea44b9d771099d246/ssh2_python-1.1.2.post1-cp310-cp310-win_amd64.whl", hash = "sha256:b1012086f8f48a215f9d646799da352431ec5ac246f284d1c00d30a45ebcdf7b", size = 4222731, upload-time = "2025-01-23T03:13:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f0/fa5de562be7130375c445561074aed6f5b214c6c45ef7f1f61a13625c2cf/ssh2_python-1.1.2.post1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:52efc9a017016d35a3bb3c45ef292bfcf2e4c7f73866fc3b39be7e41ac14d440", size = 1844981, upload-time = "2025-01-23T03:09:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/eb973a72c5c3664f0d47f83de96746e10e03fc10b9df05510d8c19d63f5f/ssh2_python-1.1.2.post1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:e388d573324254d73d410a44549cdbacb4b7fa970a421132f1d8e8ca01d42967", size = 2326239, upload-time = "2025-01-23T03:07:36.183Z" }, - { url = "https://files.pythonhosted.org/packages/1d/de/4ea6ff0d2f48a118875a11d8af98d644d6556062e3518e24ca05fae84991/ssh2_python-1.1.2.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725aafe3f6423c44e33d63e38c0f0990b259664404c6b859cd19c93203bfaa3e", size = 4975617, upload-time = "2025-01-23T03:18:42.272Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/cd7f9b4ff2ca0539ac4c900e1d00160e97b00b77078f9a9ddb36847f0231/ssh2_python-1.1.2.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d821b1b556afa0d27b82de79f4a88e11dc627689dc274ca9b846c8aa79253ab5", size = 4619186, upload-time = "2025-01-23T03:18:59.293Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2b/a8478bf90c35d86a85dd5e13e188cea2c81fddfbc3efb16a2fec37d6bee9/ssh2_python-1.1.2.post1-cp311-cp311-win_amd64.whl", hash = "sha256:9e4aaeab5ade22324522314c6992f1a07e1d30fb109c2d3eb4eac6a15df501f3", size = 4223589, upload-time = "2025-01-23T03:13:08.366Z" }, - { url = "https://files.pythonhosted.org/packages/3e/15/91daac3eda23220ee323364bcf93dd8972a74870597d45b6fced78c1e66e/ssh2_python-1.1.2.post1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3892d56a340df0dbc4f57d2debb7e17a5b93b171af27b2b71d4b3351304863ff", size = 3049981, upload-time = "2025-01-23T03:11:56.087Z" }, - { url = "https://files.pythonhosted.org/packages/11/13/aef295d9ef0eba407864173c21f0c1961099265495721df843405e1f5bf8/ssh2_python-1.1.2.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:139520fd2f12cf16cbc46ccc966af06d44864fc750f31f2da0d1aafffd163057", size = 5146630, upload-time = "2025-01-23T03:18:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/78/d5/2e3105d3d6dcc23f99fb3a96dadefdf0f2edbb770489bd14c92250d07c2c/ssh2_python-1.1.2.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19b8c83a1e923e5b4fac591cba86a09c42ffa5d0ba21456ad911f43936001e24", size = 4801706, upload-time = "2025-01-23T03:19:02.286Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/cd685009f12243d76fe4a5249a17f5a3229989d3163c44a344007e1e36b3/ssh2_python-1.1.2.post1-cp312-cp312-win_amd64.whl", hash = "sha256:84694163693310aac0482ab131d1743b5459dfe0de26201dc20e2e6e31168bc0", size = 4221651, upload-time = "2025-01-23T03:13:10.665Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ab/0c8a64ede32a78e97dd575290a4561dd84da4e0cafc27406db4f93b368dd/ssh2_python-1.1.2.post1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be981b8b814fcb0fbbcb1a01cd309405509a084469bd7158d67d7c13fbc273", size = 5063791, upload-time = "2025-01-23T03:18:47.129Z" }, - { url = "https://files.pythonhosted.org/packages/4e/47/a85134c2afc1aa675fdea20c310730f684ddd9f343c5e8211e53cd3e8d4e/ssh2_python-1.1.2.post1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b193f7b83f0ac62e49cc096e1eb6087a409097a71b5805ef6b588463cfd741c", size = 4719238, upload-time = "2025-01-23T03:19:05.006Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3b/12db397a7935d743b4b4c51ab7be9bc5289c3466e2eba6acd5f5bc00afa6/ssh2_python-1.1.2.post1-cp313-cp313-win_amd64.whl", hash = "sha256:431a549ee28a5cefd6facd1eec7a47e1125fb34b05628b2092eeb7799755e470", size = 4210838, upload-time = "2025-01-23T03:13:12.869Z" }, - { url = "https://files.pythonhosted.org/packages/5e/f0/06db27d8b59a9b589065feb6a73603164cd65a0f01cf9febe343699f8025/ssh2_python-1.1.2.post1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83aa52be4ced1102ca879a2c8a8c5663661fec9751f721ee8bfeddcd11a49e9f", size = 3320277, upload-time = "2025-01-23T03:19:00.723Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ff/aba8dcd4d96d030feb5d28f0feff5aa60b95a832725d617f72900d251d2e/ssh2_python-1.1.2.post1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba3b5cb8763552118afafddd613dd0e62119313e1a799d0748dab0d3c512f9", size = 2965281, upload-time = "2025-01-23T03:19:18.824Z" }, +version = "1.2.0.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ab/8105fdee39bee50e505c9c97e117353e19ab18c250307ec29a7dee46e46c/ssh2_python-1.2.0.post1.tar.gz", hash = "sha256:f981465ace35f96e0935b36091b61d17689d9e0e4f29b33a50882b92c45ddcbd", size = 2242329, upload-time = "2025-10-12T13:21:17.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/59/20a8c6598ca4b6c15a160d0ad661b0d1ef2a9841ad73295c61db1dc33d00/ssh2_python-1.2.0.post1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f7f79e5a2c804ebe65eb7daaeb5d9b127539037c09f2ec145e46dad0f2121acb", size = 5159582, upload-time = "2025-10-12T13:21:38.984Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0b/19e751188595b4a89c2dd6040e395a78b75661df5ac731b0afe8a0a001cd/ssh2_python-1.2.0.post1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92f2021944cee2c14e1763a2da96049bcca9e5a3fabe5103020e49afbca9b928", size = 4797428, upload-time = "2025-10-12T13:21:05.164Z" }, + { url = "https://files.pythonhosted.org/packages/07/35/7b923f2992855e22e56c90dbbc551ebd4b4fbc5353c08d8c3e62f150392e/ssh2_python-1.2.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:5866691c41aad29c4d46d86ed530f4d21bbaff63e66fefdf62189ba74777652e", size = 4232888, upload-time = "2025-10-12T13:19:03.713Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/a99e7d72e06edf30988161d6780562dcc93c8a70ff3c393e9697fdf83ed7/ssh2_python-1.2.0.post1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:15d8e0c78e0a7fc154e930ef23fa0250c24a0946507484f8064662f3d20fa1db", size = 2124170, upload-time = "2025-10-12T13:14:16.519Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/28aa5f91ba897ecaae75657ef564dc6b1abf9b273cdee6efd55a13e30e95/ssh2_python-1.2.0.post1-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:8b47215a05ec20ab7591143d7ccd6c75e1a0543ca88533a3c23fe6734f56c133", size = 2139739, upload-time = "2025-10-12T13:15:48.019Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e6/8736f6e54d8f4929d5bcd128403a9b324c203b5f0dc848e43966455e822b/ssh2_python-1.2.0.post1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bf2b086e31360d94032888ee541fcaeceae8512a12549395b0d29185ea965b18", size = 5239684, upload-time = "2025-10-12T13:21:40.284Z" }, + { url = "https://files.pythonhosted.org/packages/98/d4/e219702781cfab575e4d8a1b61f762ba6b5f451e137d812b79c9f8a064be/ssh2_python-1.2.0.post1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe103dabbaa166357251626bf83cb91be30a430506d8607db88c0ac8c4a3974e", size = 4878637, upload-time = "2025-10-12T13:21:06.449Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8f/63f1a1d27060de6161632ac8270563dfdf4570cb91a1dfa017244597e10a/ssh2_python-1.2.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:9d37b9a0e020da7c3a571a6c7ebb5813c93289e5f6fa67c8683faa161f7c44c2", size = 4234188, upload-time = "2025-10-12T13:19:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/8e359e3552ff99294eb032c32e4df93befbda7e559464cb21ebd410e4ffb/ssh2_python-1.2.0.post1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d8b24562ca03ef3b34e33c7e1983dba5400b5104c710c1152e4fa38e2a07284", size = 2739402, upload-time = "2025-10-12T13:12:20.847Z" }, + { url = "https://files.pythonhosted.org/packages/53/cb/63d2eaed35f18c2e0ac38cb976fa9d5a09851cd90f50b4fc2fa4f9743774/ssh2_python-1.2.0.post1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5bd9b71bbb5c774d6a636dc06e6a31b4a2054daa7990ce59287ec21d31ae1cb4", size = 5426089, upload-time = "2025-10-12T13:21:41.532Z" }, + { url = "https://files.pythonhosted.org/packages/41/56/cb4d6c28f7729b526c65736294222d9795fe69e8983e9573fc1171c54956/ssh2_python-1.2.0.post1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fcedb17e4ecc722b39a404fb50897b219e7cd72c1806750b1f8e7e34a6c0a95", size = 5061859, upload-time = "2025-10-12T13:21:08.02Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/867101dd3bef55c7152db60694c4f8bd953bec0072014212fbc2dab160c3/ssh2_python-1.2.0.post1-cp312-cp312-win_amd64.whl", hash = "sha256:46b5c36ae21c9ce84bff63e01f906f035939fb7fabdd4d67a50e7f6ee7d6c5dc", size = 4233777, upload-time = "2025-10-12T13:19:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/75/a0/aa9f1e6e42d3e3ceb2d8de3ac85535966efba08f45c0f88bff71013a4ba6/ssh2_python-1.2.0.post1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9026146fbeba2439263d29131850e4c02b8845ae5f81c7f0c21421bba92b3a1a", size = 5360692, upload-time = "2025-10-12T13:21:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5055064dab6b273c2fd3ae969050423f223828006a9611eeaf588977e204/ssh2_python-1.2.0.post1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:de3456b4940e5822bb65a526d7881af552f0a0de7dc69714526e294623c549b7", size = 4984822, upload-time = "2025-10-12T13:21:09.685Z" }, + { url = "https://files.pythonhosted.org/packages/eb/87/19a49ea7e81bab56f726da04d17bb9d3eb7ee2e32bf7de1b77186d05b2d9/ssh2_python-1.2.0.post1-cp313-cp313-win_amd64.whl", hash = "sha256:3558f8cbb5934a8e15c1de1c89214d952007be6937029c21960956f3417ef5f5", size = 4224015, upload-time = "2025-10-12T13:19:07.629Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/2f5c757d826492ffdc73c4e8b426db693cb6fee1af3c9606ad1401e392a1/ssh2_python-1.2.0.post1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6abfd574f557767a6e9bccd717cb2906b1fe6f56dace4aa62e1cebe002048665", size = 5346267, upload-time = "2025-10-12T13:21:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/77/3b/966a19024c35749f2acb58c07623e754d031ebbe8c759b972a3417ba7d58/ssh2_python-1.2.0.post1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42ac53dc85bc84573fae559090badb39732014f296cc213cc6cc2d9925f28685", size = 4951927, upload-time = "2025-10-12T13:21:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/75/98/55cd51f1b08b2c78abfa48f92cb53161a947d76e85bf05d66a249cd87867/ssh2_python-1.2.0.post1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7376604f64df06fcc397dd38337fe7271b807c2d3b8394459ffcccb7f38b84", size = 5725241, upload-time = "2025-10-12T13:21:46.047Z" }, + { url = "https://files.pythonhosted.org/packages/80/e4/9436676cee8592978a646c9adcbe4412f69d1cd750bef9d4f2c795979220/ssh2_python-1.2.0.post1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a2f49996543b21af7e94b4296695ef5a3da59394f07c4e1178172533a3a46ee", size = 5266355, upload-time = "2025-10-12T13:21:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/bb/04/ecc0b69cc2adce9048cdc500b84ffc93e87185a31e726140ffed6deb6061/ssh2_python-1.2.0.post1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:444190e9c92e8429a3aefd7fb40190ae1f1c3c766b53c5a1bbddcf7e08d7ca6a", size = 3323498, upload-time = "2025-10-12T13:21:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/14/81/8c7cea6395b1ce03aaced3682c1170c0b0e407e5040dfdb6a5c6fa66131b/ssh2_python-1.2.0.post1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:841a8f4117dd075e1ae50f20933097d0e0e8873451a8c52546f5e69c4b0582d6", size = 2970478, upload-time = "2025-10-12T13:21:16.546Z" }, ] [[package]] name = "stevedore" -version = "5.5.0" +version = "5.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/5f/8418daad5c353300b7661dd8ce2574b0410a6316a8be650a189d5c68d938/stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73", size = 513878, upload-time = "2025-08-25T12:54:26.806Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/c5/0c06759b95747882bb50abda18f5fb48c3e9b0fbfc6ebc0e23550b52415d/stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf", size = 49518, upload-time = "2025-08-25T12:54:25.445Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, ] [[package]] name = "tomli" -version = "2.2.1" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tox" +version = "4.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/bf/0e4dbd42724cbae25959f0e34c95d0c730df03ab03f54d52accd9abfc614/tox-4.32.0.tar.gz", hash = "sha256:1ad476b5f4d3679455b89a992849ffc3367560bbc7e9495ee8a3963542e7c8ff", size = 203330, upload-time = "2025-10-24T18:03:38.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/cc/e09c0d663a004945f82beecd4f147053567910479314e8d01ba71e5d5dea/tox-4.32.0-py3-none-any.whl", hash = "sha256:451e81dc02ba8d1ed20efd52ee409641ae4b5d5830e008af10fe8823ef1bd551", size = 175905, upload-time = "2025-10-24T18:03:36.337Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tox" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "urllib3" -version = "2.4.0" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" }, +] + +[[package]] +name = "uv" +version = "0.9.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/b8/63e4ad24d7ef24ef1de10cb2db9ff0f74b2cceb4bd71c4b3909297d40967/uv-0.9.15.tar.gz", hash = "sha256:241a57d8ce90273d0ad8460897e1b2250bd4aa6bafe72fd8be07fbc3a7662f3d", size = 3788825, upload-time = "2025-12-03T01:33:06.404Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e7/5c7fcf7a49884273c18a54b68ffd5f05775cb0e59aeeb2801f2b6d31787b/uv-0.9.15-py3-none-linux_armv6l.whl", hash = "sha256:4ccf2aa7f2e0fcb553dccc8badceb2fc533000e5baf144fd982bb9be88b304b8", size = 20936584, upload-time = "2025-12-03T01:32:58.983Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/5c376318f57dd4fc58bb01db870f9e564a689d479ddc0440731933286740/uv-0.9.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:944cd6d500974f9693994ec990c5b263a185e66cbc1cbd230f319445f8330e4e", size = 20000398, upload-time = "2025-12-03T01:33:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c7/6af7e5dc21902eb936a54c037650f070cea015d742b3a49e82f53e6f32c4/uv-0.9.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ec4716fee5db65cfc78016d07f6fddb9b1fa29a0471fe67fe6676e2befee3215", size = 18440619, upload-time = "2025-12-03T01:32:56.388Z" }, + { url = "https://files.pythonhosted.org/packages/88/73/8364801f678ba58d1a31ec9c8e0bfc407b48c0c57e0e3618e8fbf6b285f6/uv-0.9.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2b9ad2581a90b7b2ed3f5a3b2657c3cf84119bdf101e1a1c49a2f38577eecb1b", size = 20326432, upload-time = "2025-12-03T01:32:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/9c13d81005b00dcc759f7ea4b640b1efff8ecebbf852df90c2f237373ed5/uv-0.9.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6a837537294e283ddbb18c494dbc085afca3e29d2176059140b7b4e94e485fd", size = 20531552, upload-time = "2025-12-03T01:33:18.894Z" }, + { url = "https://files.pythonhosted.org/packages/b1/24/99c26056300c83f0d542272c02465937965191739d44bf8654d09d2d296f/uv-0.9.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:214013314564347ad629a5cfcd28b962038fc23c72155d7a798313f6b9f64f81", size = 21428020, upload-time = "2025-12-03T01:33:10.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4a/15dd32d695eae71cb4c09af6e9cbde703674824653c9f2963b068108b344/uv-0.9.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b32c1ac11ea80ab4cc6ea61029880e1319041d2b3783f2478c8eadfc9a9c9d5a", size = 23061719, upload-time = "2025-12-03T01:32:48.914Z" }, + { url = "https://files.pythonhosted.org/packages/51/02/83c179d6a5cfee0c437dd1d73b515557c6b2b7ab19fb9421420c13b10bc8/uv-0.9.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d99dfe8af75fed7fd6b7f6a8a81ba26ac88e046c5cb366184c2b53b023b070", size = 22609336, upload-time = "2025-12-03T01:32:41.645Z" }, + { url = "https://files.pythonhosted.org/packages/a3/77/c620febe2662ab1897c2ef7c95a5424517efc456b77a1f75f6da81b4e542/uv-0.9.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5375c95a70fc76104fc178771cd4a8db1dab55345c7162c3d2d47ca2993c4bb", size = 21663700, upload-time = "2025-12-03T01:32:36.335Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/4273d02565a4e86f238e9fee23e6c5c3bb7b5c237c63228e1b72451db224/uv-0.9.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9c1cefdd6e878baa32ff7a7d1ef463690750ff45d899fbb6bd6705e8329b00a", size = 21714799, upload-time = "2025-12-03T01:33:01.785Z" }, + { url = "https://files.pythonhosted.org/packages/61/ae/29787af8124d821c1a88bb66612c24ff6180309d35348a4915214c5078d3/uv-0.9.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:b6d342a4610e7dbc3450c8000313ee7693905ddee12a65a34fdbcd8418a852a5", size = 20454218, upload-time = "2025-12-03T01:33:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/ab906a1e530f0fbb7042e82057d7c08ef46131deb75d5fef2133de6725c5/uv-0.9.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dea4fd678d48b9790234de43b2e5e4a445f9541fd361db7dc1a2cac861c9717a", size = 21549370, upload-time = "2025-12-03T01:32:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/19/11/20ea6dc5ca56f2ad9a8f1b5674f84c38a45a28ccf880aa7c1abb226355a6/uv-0.9.15-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bae65f4748cacc20ea7f93b94a9ba82dd5505a98cf61e33d75e2669c6aefbfc5", size = 20502715, upload-time = "2025-12-03T01:32:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ed98b5f48be2aeb63dc8853bc777b739c18277d0b950196b4d46f7b4d74a/uv-0.9.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5473c1095a697b7c7996a312232f78e81cf71e5afc30c889e46a486fe298d853", size = 20890379, upload-time = "2025-12-03T01:32:28.035Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/a97468ed93b80a50ea97a717662be4b841e7149bc68197ded087bcdd9e36/uv-0.9.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a0ef2e2bcf807aebd70e3a87bde618d0a01f07f627f5da0b0b457d7be2124843", size = 21921653, upload-time = "2025-12-03T01:32:31.016Z" }, + { url = "https://files.pythonhosted.org/packages/94/e8/ad85878d4e789c40b75c9f670eba52c7e5e63393f31e1c1a7246849595a2/uv-0.9.15-py3-none-win32.whl", hash = "sha256:5efa39d9085f918d17869e43471ccd4526372e71d945b8d7cd3c8867ce6eab33", size = 19762699, upload-time = "2025-12-03T01:32:39.104Z" }, + { url = "https://files.pythonhosted.org/packages/bc/37/e15a46f880b2c230f7d6934f046825ebe547f90008984c861e84e2ef34c4/uv-0.9.15-py3-none-win_amd64.whl", hash = "sha256:e46f36c80353fb406d4c0fb2cfe1953b4a01bfb3fc5b88dd4f763641b8899f1a", size = 21758441, upload-time = "2025-12-03T01:32:33.722Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fe/af3595c25dfaa85daff11ed0c8ca0fc1d088a426c65ef93d3751490673fd/uv-0.9.15-py3-none-win_arm64.whl", hash = "sha256:8f14456f357ebbf3f494ae5af41e9b306ba66ecc81cb2304095b38d99a1f7d28", size = 20203340, upload-time = "2025-12-03T01:32:44.184Z" }, ] [[package]] name = "virtualenv" -version = "20.30.0" +version = "20.35.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945, upload-time = "2025-03-31T16:33:29.185Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461, upload-time = "2025-03-31T16:33:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] [[package]] name = "wcwidth" -version = "0.2.13" +version = "0.2.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] From 0a7d2942801907f5997f40cacdb64c95be216c1f Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sat, 6 Dec 2025 09:32:27 -0500 Subject: [PATCH 02/10] Refactor helpers into separate modules - Split the monolithic helpers.py file into separate modules by functionality: - dict_utils.py: Dictionary manipulation utilities - file_utils.py: File handling utilities - inventory.py: Inventory management utilities - results.py: Result and MockStub classes - misc.py: CLI and miscellaneous utilities - Created broker/helpers/__init__.py to re-export all functions for backward compatibility. - Updated logging configuration to use standard python logging. - Addressed code review feedback regarding imports and type checking. --- .gitignore | 1 + broker/helpers.py | 785 ----------------------------------- broker/helpers/__init__.py | 103 +++++ broker/helpers/dict_utils.py | 103 +++++ broker/helpers/file_utils.py | 153 +++++++ broker/helpers/inventory.py | 160 +++++++ broker/helpers/misc.py | 261 ++++++++++++ broker/helpers/results.py | 140 +++++++ 8 files changed, 921 insertions(+), 785 deletions(-) delete mode 100644 broker/helpers.py create mode 100644 broker/helpers/__init__.py create mode 100644 broker/helpers/dict_utils.py create mode 100644 broker/helpers/file_utils.py create mode 100644 broker/helpers/inventory.py create mode 100644 broker/helpers/misc.py create mode 100644 broker/helpers/results.py diff --git a/.gitignore b/.gitignore index 761d76d3..a5c518f3 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,4 @@ ENV/ inventory.yaml *.bak /bin/* +.venv diff --git a/broker/helpers.py b/broker/helpers.py deleted file mode 100644 index a7238b93..00000000 --- a/broker/helpers.py +++ /dev/null @@ -1,785 +0,0 @@ -"""Miscellaneous helpers live here.""" - -import collections -from collections import UserDict, namedtuple -from collections.abc import MutableMapping -from contextlib import contextmanager -from copy import deepcopy -import getpass -import inspect -from io import BytesIO -import json -import logging -import os -from pathlib import Path -import sys -import tarfile -import threading -import time -from uuid import uuid4 - -import click -from rich.table import Table -from ruamel.yaml import YAML - -from broker import exceptions -from broker.settings import clone_global_settings - -logger = logging.getLogger(__name__) - -FilterTest = namedtuple("FilterTest", "haystack needle test") -INVENTORY_LOCK = threading.Lock() - -yaml = YAML() -yaml.default_flow_style = False -yaml.sort_keys = False - -SPECIAL_INVENTORY_FIELDS = {} # use the _special_inventory_field decorator to add new fields - - -def _special_inventory_field(action_name): - """Register inventory field actions.""" - - def decorator(func): - SPECIAL_INVENTORY_FIELDS[action_name] = func - return func - - return decorator - - -def clean_dict(in_dict): - """Remove entries from a dict where value is None.""" - return {k: v for k, v in in_dict.items() if v is not None} - - -def merge_dicts(dict1, dict2): - """Merge two nested dictionaries together. - - :return: merged dictionary - """ - if not isinstance(dict1, MutableMapping) or not isinstance(dict2, MutableMapping): - return dict1 - dict1 = clean_dict(dict1) - dict2 = clean_dict(dict2) - merged = {} - dupe_keys = dict1.keys() & dict2.keys() - for key in dupe_keys: - merged[key] = merge_dicts(dict1[key], dict2[key]) - for key in dict1.keys() - dupe_keys: - merged[key] = deepcopy(dict1[key]) - for key in dict2.keys() - dupe_keys: - merged[key] = deepcopy(dict2[key]) - return merged - - -def flatten_dict(nested_dict, parent_key="", separator="_"): - """Flatten a nested dictionary, keeping nested notation in key. - - { - 'key': 'value1', - 'another': { - 'nested': 'value2', - 'nested2': [1, 2, {'deep': 'value3'}] - } - } - becomes - { - "key": "value", - "another_nested": "value2", - "another_nested2": [1, 2], - "another_nested2_deep": "value3" - } - note that dictionaries nested in lists will be removed from the list. - - :return: dictionary - """ - flattened = [] - for key, value in nested_dict.items(): - new_key = f"{parent_key}{separator}{key}" if parent_key else key - if isinstance(value, dict): - flattened.extend(flatten_dict(value, new_key, separator).items()) - elif isinstance(value, list): - to_remove = [] - # avoid mutating nested structures - value = value.copy() # noqa: PLW2901 - for index, val in enumerate(value): - if isinstance(val, dict): - flattened.extend(flatten_dict(val, new_key, separator).items()) - to_remove.append(index) - for index in to_remove[::-1]: # remove from back to front - del value[index] - flattened.append((new_key, value)) - else: - flattened.append((new_key, value)) - return dict(flattened) - - -def dict_from_paths(source_dict, paths, sep="/"): - """Given a dictionary of desired keys and nested paths, return a new dictionary. - - Example: - source_dict = { - "key1": "value1", - "key2": { - "nested1": "value2", - "nested2": { - "deep": "value3" - } - } - } - paths = { - "key1": "key1", - "key2": "key2/nested2/deep" - } - returns { - "key1": "value1", - "key2": "value3" - } - """ - result = {} - for key, path in paths.items(): - if sep not in path: - result[key] = source_dict.get(path) - else: - top, rem = path.split(sep, 1) - result.update(dict_from_paths(source_dict[top], {key: rem})) - return result - - -def eval_filter(filter_list, raw_filter, filter_key="inv"): - """Run each filter through an eval to get the results.""" - filter_list = [MockStub(item) if isinstance(item, dict) else item for item in filter_list] - for raw_f in raw_filter.split("|"): - if f"@{filter_key}[" in raw_f: - # perform a list filter on the inventory - filter_list = eval( # noqa: S307 - raw_f.replace(f"@{filter_key}", filter_key), {filter_key: filter_list} - ) - filter_list = filter_list if isinstance(filter_list, list) else [filter_list] - elif f"@{filter_key}" in raw_f: - # perform an attribute filter on each host - filter_list = list( - filter( - lambda item: eval( # noqa: S307 - raw_f.replace(f"@{filter_key}", filter_key), {filter_key: item} - ), - filter_list, - ) - ) - return [dict(item) if isinstance(item, MockStub) else item for item in filter_list] - - -def resolve_nick(nick, broker_settings=None): - """Check if the nickname exists. Used to define broker arguments. - - :param nick: String representing the name of a nick - :param broker_settings: Optional settings object to use instead of global settings - - :return: a dictionary mapping argument names and values - """ - _settings = broker_settings or clone_global_settings() - nick_names = _settings.get("NICKS") or {} - if nick in nick_names: - return _settings.NICKS[nick].to_dict() - else: - raise exceptions.UserError(f"Unknown nick: {nick}") - - -def load_file(file, warn=True): - """Verify the existence of and load data from json and yaml files.""" - file = Path(file) - if not file.exists() or file.suffix not in (".json", ".yaml", ".yml"): - if warn: - logger.warning(f"File {file.absolute()} is invalid or does not exist.") - return [] - if file.suffix == ".json": - return json.loads(file.read_text()) - elif file.suffix in (".yaml", ".yml"): - return yaml.load(file) - - -def resolve_file_args(broker_args): - """Check for files being passed in as values to arguments then attempt to resolve them. - - If not resolved, keep arg/value pair intact. - """ - final_args = {} - # parse the eventual args_file first - if val := broker_args.pop("args_file", None): - if isinstance(val, Path) or (isinstance(val, str) and val[-4:] in ("json", "yaml", ".yml")): - if data := load_file(val): - if isinstance(data, dict): - final_args.update(data) - elif isinstance(data, list): - for d in data: - final_args.update(d) - else: - raise exceptions.BrokerError(f"No data loaded from {val}") - - for key, val in broker_args.items(): - if isinstance(val, Path) or (isinstance(val, str) and val[-4:] in ("json", "yaml", ".yml")): - if data := load_file(val): - final_args.update({key: data}) - else: - final_args.update({key: val}) - else: - final_args.update({key: val}) - return final_args - - -def load_inventory(filter=None): - """Load all local hosts in inventory. - - :param filter: A filter string to apply to the inventory. - - :return: list of dictionaries - """ - from broker.settings import inventory_path - - inv_data = load_file(inventory_path, warn=False) - if inv_data and filter: - inv_data = eval_filter(inv_data, filter) - return inv_data or [] - - -def update_inventory(add=None, remove=None): - """Update list of local hosts in the checkout interface. - - :param add: list of dictionaries representing new hosts - :param remove: list of strings representing hostnames or names to be removed - - :return: no return value - """ - from broker.settings import inventory_path - - if add and not isinstance(add, list): - add = [add] - elif not add: - add = [] - if remove and not isinstance(remove, list): - remove = [remove] - with INVENTORY_LOCK: - inv_data = load_inventory() - if inv_data: - inventory_path.unlink() - - if remove: - for host in inv_data[::-1]: - if host["hostname"] in remove or host.get("name") in remove: - # iterate through new hosts and update with old host data if it would nullify - for new_host in add: - if host["hostname"] == new_host["hostname"] or host.get( - "name" - ) == new_host.get("name"): - # update missing data in the new_host with the old_host data - new_host.update(merge_dicts(new_host, host)) - inv_data.remove(host) - if add: - inv_data.extend(add) - - inventory_path.touch() - yaml.dump(inv_data, inventory_path) - - -def yaml_format(in_struct, force_yaml_dict=False): - """Convert a yaml-compatible structure to a yaml dumped string. - - :param in_struct: yaml-compatible structure or string containing structure - :param force_yaml_dict: force the in_struct to be converted to a dictionary before dumping - - :return: yaml-formatted string - """ - if isinstance(in_struct, str): - # first try to load is as json - try: - in_struct = json.loads(in_struct) - except json.JSONDecodeError: - # then try yaml - in_struct = yaml.load(in_struct) - if force_yaml_dict: - in_struct = dict(in_struct) - output = BytesIO() # ruamel doesn't natively allow for string output - yaml.dump(in_struct, output) - return output.getvalue().decode("utf-8") - - -def flip_provider_actions(provider_actions): - """Flip the mapping of actions->provider to provider->actions.""" - flipped = {} - for action, (provider, _) in provider_actions.items(): - provider_name = provider.__name__ - if provider_name not in flipped: - flipped[provider_name] = [] - flipped[provider_name].append(action) - return flipped - - -def inventory_fields_to_dict(inventory_fields, host_dict, **extras): - """Convert a dicionary-like representation of inventory fields to a resolved dictionary. - - inventory fields, as set in the config look like this, in yaml: - inventory_fields: - Host: hostname | name - Provider: _broker_provider - Action: $action - OS: os_distribution os_distribution_version - - We then process that into a dictionary with inventory values like this: - { - "Host": "some.test.host", - "Provider": "AnsibleTower", - "Action": "deploy-rhel", - "OS": "RHEL 8.4" - } - - Notes: The special syntax use in Host and Action fields <$action> is a special keyword that - represents a more complex field resolved by Broker. - Also, the Host field represents a priority order of single values, - so if hostname is not present, name will be used. - Finally, spaces between values are preserved. This lets us combine multiple values in a single field. - """ - return { - name: _resolve_inv_field(field, host_dict, **extras) - for name, field in inventory_fields.items() - } - - -def _resolve_inv_field(field, host_dict, **extras): - """Real functionality for inventory_fields_to_dict, allows recursive evaluation.""" - # Users can specify multiple values to try in order of priority, so evaluate each - if "|" in field: - resolved = [_resolve_inv_field(f.strip(), host_dict, **extras) for f in field.split("|")] - for val in resolved: - if val and val != "Unknown": - return val - return "Unknown" - # Users can combine multiple values in a single field, so evaluate each - if " " in field: - return " ".join(_resolve_inv_field(f, host_dict, **extras) for f in field.split()) - # Some field values require special handling beyond what the existing syntax allows - if special_field_func := SPECIAL_INVENTORY_FIELDS.get(field): - return special_field_func(host_dict, **extras) - # Otherwise, try to get the value from the host dictionary - return dict_from_paths(host_dict, {"_": field}, sep=".")["_"] or "Unknown" - - -@_special_inventory_field("$action") -def get_host_action(host_dict, provider_actions=None, **_): - """Get a more focused set of fields from the host inventory.""" - if not provider_actions: - return "$actionError" - # Flip the mapping of actions->provider to provider->actions - flipped_actions = {} - for action, (provider, _) in provider_actions.items(): - provider_name = provider.__name__ - if provider_name not in flipped_actions: - flipped_actions[provider_name] = [] - flipped_actions[provider_name].append(action) - # Get the host's action, based on its provider - provider = host_dict["_broker_provider"] - for opt in flipped_actions[provider]: - if action := host_dict["_broker_args"].get(opt): - return action - return "Unknown" - - -def kwargs_from_click_ctx(ctx): - """Convert a Click context object to a dictionary of keyword arguments.""" - # if users use `=` to note arg=value assignment, then we need to split it - _args = [] - for arg in ctx.args: - if "=" in arg: - _args.extend(arg.split("=")) - else: - _args.append(arg) - ctx.args = _args - # if additional arguments were passed, include them in the broker args - # strip leading -- characters - return { - (key[2:] if key.startswith("--") else key): val - for key, val in zip(ctx.args[::2], ctx.args[1::2]) - } - - -class Emitter: - """Class that provides a simple interface to emit messages to a json-formatted file. - - This module also has an instance of this class called "emit" that should be used - instead of this class directly. - - Usage examples: - helpers.emit(key=value, another=5) - helpers.emit({"key": "value", "another": 5}) - """ - - EMIT_LOCK = threading.Lock() - - def __init__(self, emit_file=None): - """Can empty init and set the file later.""" - self.file = None - if emit_file: - self.file = self.set_file(emit_file) - - def set_file(self, file_path): - """Set the file to emit to.""" - if file_path: - self.file = Path(file_path) - self.file.parent.mkdir(exist_ok=True, parents=True) - if self.file.exists(): - self.file.unlink() - self.file.touch() - - def emit_to_file(self, *args, **kwargs): - """Emit data to the file, keeping existing data in-place.""" - if not self.file: - return - for arg in args: - if not isinstance(arg, dict): - raise exceptions.BrokerError(f"Received an invalid data emission {arg}") - kwargs.update(arg) - for key, val in kwargs.items(): - if getattr(val, "json", None): - kwargs[key] = val.json - with self.EMIT_LOCK: - curr_data = json.loads(self.file.read_text() or "{}") - curr_data.update(kwargs) - self.file.write_text(json.dumps(curr_data, indent=4, sort_keys=True)) - - def __call__(self, *args, **kwargs): - """Allow emit to be used like a function.""" - return self.emit_to_file(*args, **kwargs) - - -emit = Emitter() - - -class MockStub(UserDict): - """Test helper class. Allows for both arbitrary mocking and stubbing.""" - - def __init__(self, in_dict=None): - """Initialize the class and all nested dictionaries.""" - if in_dict is None: - in_dict = {} - for key, value in in_dict.items(): - if isinstance(value, dict): - setattr(self, key, MockStub(value)) - elif type(value) in (list, tuple): - setattr( - self, - key, - [MockStub(x) if isinstance(x, dict) else x for x in value], - ) - else: - setattr(self, key, value) - super().__init__(in_dict) - - def __getattr__(self, name): - """Fallback to returning self if attribute doesn't exist.""" - return self - - def __getitem__(self, key): - """Get an item from the dictionary-like object. - - If the key is a string, this method will attempt to get an attribute with that name. - If the key is not found, this method will return the object itself. - """ - if isinstance(key, str): - item = getattr(self, key, self) - try: - item = super().__getitem__(key) - except KeyError: - item = self - return item - - def __call__(self, *args, **kwargs): - """Allow MockStub to be used like a function.""" - return self - - def __hash__(self): - """Return a hash value for the object. - - The hash value is computed using the hash value of all hashable attributes of the object. - """ - return hash( - tuple(kp for kp in self.__dict__.items() if isinstance(kp[1], collections.abc.Hashable)) - ) - - -def update_log_level(ctx, param, value): - """Update the log level and file logging settings for the Broker. - - Args: - ctx: The Click context object. - param: The Click parameter object. - value: The new log level value. - """ - from broker.logging import setup_logging - - setup_logging(console_level=value) - - -def set_emit_file(ctx, param, value): - """Update the file that the Broker emits data to.""" - emit.set_file(value) - - -def fork_broker(): - """Fork the Broker process to run in the background.""" - pid = os.fork() - if pid: - logger.info(f"Running broker in the background with pid: {pid}") - sys.exit(0) - from broker.logging import setup_logging - - setup_logging(console_level="silent", file_level="silent") - - -def handle_keyboardinterrupt(*args): - """Handle keyboard interrupts gracefully. - - Offer the user a choice between keeping Broker alive in the background, killing it, or resuming execution. - """ - choice = click.prompt( - "\nEnding Broker while running may not end processes being monitored.\n" - "Would you like to switch Broker to run in the Background, Kill it, or Resume execution?\n", - type=click.Choice(["b", "k", "r"]), - default="r", - ).lower() - if choice == "b": - fork_broker() - elif choice == "k": - raise exceptions.BrokerError("Broker killed by user.") - elif choice == "r": - click.echo("Resuming execution...") - - -def translate_timeout(timeout): - """Allow for flexible timeout definitions, converts other units to ms. - - acceptable units are (s)econds, (m)inutes, (h)ours, (d)ays - """ - if isinstance(timeout, str): - timeout, unit = int(timeout[:-1]), timeout[-1] - if unit == "d": - timeout *= 24 - unit = "h" - if unit == "h": - timeout *= 60 - unit = "m" - if unit == "m": - timeout *= 60 - unit = "s" - if unit == "s": - timeout *= 1000 - return timeout if isinstance(timeout, int) else 0 - - -def simple_retry( - cmd, cmd_args=None, cmd_kwargs=None, max_timeout=60, _cur_timeout=1, terminal_exceptions=None -): - """Re(Try) a function given its args and kwargs up until a max timeout.""" - cmd_args = cmd_args if cmd_args else [] - cmd_kwargs = cmd_kwargs if cmd_kwargs else {} - terminal_exceptions = terminal_exceptions or () - - try: - return cmd(*cmd_args, **cmd_kwargs) - except terminal_exceptions: - raise - except Exception as err: - new_wait = _cur_timeout * 2 - if new_wait > max_timeout: - raise err - logger.warning( - f"Tried {cmd=} with {cmd_args=}, {cmd_kwargs=} but received {err=}" - f"\nTrying again in {_cur_timeout} seconds." - ) - time.sleep(_cur_timeout) - simple_retry(cmd, cmd_args, cmd_kwargs, max_timeout, new_wait, terminal_exceptions) - - -class FileLock: - """Basic file locking class that acquires and releases locks. - - Recommended usage is the context manager which will handle everything for you - - with FileLock("basic_file.txt"): - Path("basic_file.txt").write_text("some text") - - If a lock is already in place, FileLock will wait up to seconds - """ - - def __init__(self, file_name, timeout=10): - self.lock = Path(f"{file_name}.lock") - self.timeout = timeout - - def wait_file(self): - """Wait for the lock file to be released, then acquire it.""" - timeout_after = time.time() + self.timeout - while self.lock.exists(): - if time.time() <= timeout_after: - time.sleep(1) - else: - raise exceptions.BrokerError( - f"Timeout while waiting for lock release: {self.lock.absolute()}" - ) - self.lock.touch() - - def return_file(self): - """Release the lock file.""" - self.lock.unlink() - - def __enter__(self): # noqa: D105 - self.wait_file() - - def __exit__(self, *tb_info): # noqa: D105 - self.return_file() - - -class Result: - """Dummy result class for presenting results in dot access.""" - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - """Return a string representation of the object.""" - return f"stdout:\n{self.stdout}\nstderr:\n{self.stderr}\nstatus: {self.status}" - - @classmethod - def from_ssh(cls, stdout, channel): - """Create a Result object from an SSH channel.""" - return cls( - stdout=stdout, - status=channel.get_exit_status(), - stderr=channel.read_stderr()[1].decode("utf-8"), - ) - - @classmethod - def from_duplexed_exec(cls, duplex_exec, runtime=None): - """Create a Result object from a duplexed exec object from podman or docker.""" - if runtime == "podman": - status, (stdout, stderr) = duplex_exec - return cls( - status=status, - stdout=stdout.decode("utf-8") if stdout else "", - stderr=stderr.decode("utf-8") if stderr else "", - ) - - if duplex_exec.output[0]: - stdout = duplex_exec.output[0].decode("utf-8") - else: - stdout = "" - if duplex_exec.output[1]: - stderr = duplex_exec.output[1].decode("utf-8") - else: - stderr = "" - return cls( - status=duplex_exec.exit_code, - stdout=stdout, - stderr=stderr, - ) - - @classmethod - def from_nonduplexed_exec(cls, nonduplex_exec): - """Create a Result object from a nonduplexed exec object from the docker library.""" - return cls( - status=nonduplex_exec.exit_code, - stdout=nonduplex_exec.output.decode("utf-8"), - stderr="", - ) - - -def find_origin(): - """Move up the call stack to find tests, fixtures, or cli invocations. - - Additionally, return the jenkins url, if it exists. - """ - prev, jenkins_url = None, os.environ.get("BUILD_URL") - for frame in inspect.stack(): - if frame.function == "checkout" and frame.filename.endswith("broker/commands.py"): - return f"broker_cli:{getpass.getuser()}", jenkins_url - if frame.function.startswith("test_"): - return f"{frame.function}:{frame.filename}", jenkins_url - if frame.function == "call_fixture_func": - # attempt to find the test name from the fixture's request object - if request := _frame.frame.f_locals.get("request"): # noqa: F821 - return f"{prev} for {request.node._nodeid}", jenkins_url - # otherwise, return the fixture name and filename - return prev or "Uknown fixture", jenkins_url - prev, _frame = f"{frame.function}:{frame.filename}", frame - return f"Unknown origin by {getpass.getuser()}", jenkins_url - - -@contextmanager -def data_to_tempfile(data, path=None, as_tar=False): - """Write data to a temporary file and return the path.""" - path = Path(path or uuid4().hex[-10]) - logger.debug(f"Creating temporary file {path.absolute()}") - if isinstance(data, bytes): - path.write_bytes(data) - elif isinstance(data, str): - path.write_text(data) - else: - raise TypeError(f"data must be bytes or str, not {type(data)}") - if as_tar: - tar = tarfile.open(path) - yield tarfile.open(path) - tar.close() - else: - yield path - path.unlink() - - -@contextmanager -def temporary_tar(paths): - """Create a temporary tar file and return the path.""" - temp_tar = Path(f"{uuid4().hex[-10]}.tar") - with tarfile.open(temp_tar, mode="w") as tar: - for path in paths: - logger.debug(f"Adding {path.absolute()} to {temp_tar.absolute()}") - tar.add(path, arcname=path.name) - yield temp_tar.absolute() - temp_tar.unlink() - - -def dictlist_to_table(dict_list, title=None, _id=False, headers=True): - """Convert a list of dictionaries to a rich table.""" - # I like pretty colors, so let's cycle through them - column_colors = ["cyan", "magenta", "green", "yellow", "blue", "red"] - curr_color = 0 - table = Table(title=title) - # construct the columns - if _id: # likely just for inventory tables - table.add_column("Id", justify="left", style=column_colors[curr_color], no_wrap=True) - curr_color += 1 - for key in dict_list[0]: # assume all dicts have the same keys - table.add_column(key, justify="left", style=column_colors[curr_color]) - curr_color += 1 - if curr_color >= len(column_colors): - curr_color = 0 - # add the rows - for id_num, data_dict in enumerate(dict_list): - row = [str(id_num)] if _id else [] - row.extend([str(value) for value in data_dict.values()]) - table.add_row(*row) - if not headers: - table.show_header = False - return table - - -def dict_to_table(in_dict, title=None, headers=None): - """Convert a dictionary into a rich table.""" - # need to normalize the values first - in_dict = {k: str(v) for k, v in in_dict.items()} - table = Table(title=title) - if isinstance(headers, tuple | list): - table.add_column(headers[0], style="cyan") - table.add_column(headers[1], style="magenta") - else: - table.add_column("key", style="cyan") - table.add_column("value", style="magenta") - table.show_header = False - for key, val in in_dict.items(): - table.add_row(key, val) - return table diff --git a/broker/helpers/__init__.py b/broker/helpers/__init__.py new file mode 100644 index 00000000..3acacdfb --- /dev/null +++ b/broker/helpers/__init__.py @@ -0,0 +1,103 @@ +"""Miscellaneous helpers live here. + +This module provides backward-compatible imports for all helpers. +The helpers are organized into submodules by functionality: +- dict_utils: Dictionary manipulation utilities +- file_utils: File handling utilities +- inventory: Inventory management utilities +- results: Result and testing helper classes +- misc: Miscellaneous helper functions +""" + +# Dictionary utilities +from broker.helpers.dict_utils import ( + clean_dict, + dict_from_paths, + flatten_dict, + merge_dicts, +) + +# File handling utilities +from broker.helpers.file_utils import ( + FileLock, + data_to_tempfile, + load_file, + resolve_file_args, + temporary_tar, + yaml, + yaml_format, +) + +# Inventory utilities +from broker.helpers.inventory import ( + INVENTORY_LOCK, + SPECIAL_INVENTORY_FIELDS, + flip_provider_actions, + get_host_action, + inventory_fields_to_dict, + load_inventory, + update_inventory, +) + +# Miscellaneous utilities +from broker.helpers.misc import ( + Emitter, + dict_to_table, + dictlist_to_table, + emit, + find_origin, + fork_broker, + handle_keyboardinterrupt, + kwargs_from_click_ctx, + resolve_nick, + set_emit_file, + simple_retry, + translate_timeout, + update_log_level, +) + +# Result and testing classes +from broker.helpers.results import ( + FilterTest, + MockStub, + Result, + eval_filter, +) + +__all__ = [ + "INVENTORY_LOCK", + "SPECIAL_INVENTORY_FIELDS", + "Emitter", + "FileLock", + "FilterTest", + "MockStub", + "Result", + "clean_dict", + "data_to_tempfile", + "dict_from_paths", + "dict_to_table", + "dictlist_to_table", + "emit", + "eval_filter", + "find_origin", + "flatten_dict", + "flip_provider_actions", + "fork_broker", + "get_host_action", + "handle_keyboardinterrupt", + "inventory_fields_to_dict", + "kwargs_from_click_ctx", + "load_file", + "load_inventory", + "merge_dicts", + "resolve_file_args", + "resolve_nick", + "set_emit_file", + "simple_retry", + "temporary_tar", + "translate_timeout", + "update_inventory", + "update_log_level", + "yaml", + "yaml_format", +] diff --git a/broker/helpers/dict_utils.py b/broker/helpers/dict_utils.py new file mode 100644 index 00000000..b4ab5ff7 --- /dev/null +++ b/broker/helpers/dict_utils.py @@ -0,0 +1,103 @@ +"""Dictionary manipulation utilities.""" + +from collections.abc import MutableMapping +from copy import deepcopy + + +def clean_dict(in_dict): + """Remove entries from a dict where value is None.""" + return {k: v for k, v in in_dict.items() if v is not None} + + +def merge_dicts(dict1, dict2): + """Merge two nested dictionaries together. + + :return: merged dictionary + """ + if not isinstance(dict1, MutableMapping) or not isinstance(dict2, MutableMapping): + return dict1 + dict1 = clean_dict(dict1) + dict2 = clean_dict(dict2) + merged = {} + dupe_keys = dict1.keys() & dict2.keys() + for key in dupe_keys: + merged[key] = merge_dicts(dict1[key], dict2[key]) + for key in dict1.keys() - dupe_keys: + merged[key] = deepcopy(dict1[key]) + for key in dict2.keys() - dupe_keys: + merged[key] = deepcopy(dict2[key]) + return merged + + +def flatten_dict(nested_dict, parent_key="", separator="_"): + """Flatten a nested dictionary, keeping nested notation in key. + + { + 'key': 'value1', + 'another': { + 'nested': 'value2', + 'nested2': [1, 2, {'deep': 'value3'}] + } + } + becomes + { + "key": "value", + "another_nested": "value2", + "another_nested2": [1, 2], + "another_nested2_deep": "value3" + } + note that dictionaries nested in lists will be removed from the list. + + :return: dictionary + """ + flattened = [] + for key, value in nested_dict.items(): + new_key = f"{parent_key}{separator}{key}" if parent_key else key + if isinstance(value, dict): + flattened.extend(flatten_dict(value, new_key, separator).items()) + elif isinstance(value, list): + to_remove = [] + # avoid mutating nested structures + value = value.copy() # noqa: PLW2901 + for index, val in enumerate(value): + if isinstance(val, dict): + flattened.extend(flatten_dict(val, new_key, separator).items()) + to_remove.append(index) + for index in to_remove[::-1]: # remove from back to front + del value[index] + flattened.append((new_key, value)) + else: + flattened.append((new_key, value)) + return dict(flattened) + + +def dict_from_paths(source_dict, paths, sep="/"): + """Given a dictionary of desired keys and nested paths, return a new dictionary. + + Example: + source_dict = { + "key1": "value1", + "key2": { + "nested1": "value2", + "nested2": { + "deep": "value3" + } + } + } + paths = { + "key1": "key1", + "key2": "key2/nested2/deep" + } + returns { + "key1": "value1", + "key2": "value3" + } + """ + result = {} + for key, path in paths.items(): + if sep not in path: + result[key] = source_dict.get(path) + else: + top, rem = path.split(sep, 1) + result.update(dict_from_paths(source_dict[top], {key: rem})) + return result diff --git a/broker/helpers/file_utils.py b/broker/helpers/file_utils.py new file mode 100644 index 00000000..c17ab653 --- /dev/null +++ b/broker/helpers/file_utils.py @@ -0,0 +1,153 @@ +"""File handling utilities.""" + +from contextlib import contextmanager +from io import BytesIO +import json +import logging +from pathlib import Path +import tarfile +import time +from uuid import uuid4 + +from ruamel.yaml import YAML + +from broker import exceptions + +logger = logging.getLogger(__name__) +yaml = YAML() +yaml.default_flow_style = False +yaml.sort_keys = False + + +def load_file(file, warn=True): + """Verify the existence of and load data from json and yaml files.""" + file = Path(file) + if not file.exists() or file.suffix not in (".json", ".yaml", ".yml"): + if warn: + logger.warning(f"File {file.absolute()} is invalid or does not exist.") + return [] + if file.suffix == ".json": + return json.loads(file.read_text()) + elif file.suffix in (".yaml", ".yml"): + return yaml.load(file) + + +def resolve_file_args(broker_args): + """Check for files being passed in as values to arguments then attempt to resolve them. + + If not resolved, keep arg/value pair intact. + """ + final_args = {} + # parse the eventual args_file first + if val := broker_args.pop("args_file", None): + if isinstance(val, Path) or (isinstance(val, str) and val[-4:] in ("json", "yaml", ".yml")): + if data := load_file(val): + if isinstance(data, dict): + final_args.update(data) + elif isinstance(data, list): + for d in data: + final_args.update(d) + else: + raise exceptions.BrokerError(f"No data loaded from {val}") + + for key, val in broker_args.items(): + if isinstance(val, Path) or (isinstance(val, str) and val[-4:] in ("json", "yaml", ".yml")): + if data := load_file(val): + final_args.update({key: data}) + else: + final_args.update({key: val}) + else: + final_args.update({key: val}) + return final_args + + +def yaml_format(in_struct, force_yaml_dict=False): + """Convert a yaml-compatible structure to a yaml dumped string. + + :param in_struct: yaml-compatible structure or string containing structure + :param force_yaml_dict: force the in_struct to be converted to a dictionary before dumping + + :return: yaml-formatted string + """ + if isinstance(in_struct, str): + # first try to load is as json + try: + in_struct = json.loads(in_struct) + except json.JSONDecodeError: + # then try yaml + in_struct = yaml.load(in_struct) + if force_yaml_dict: + in_struct = dict(in_struct) + output = BytesIO() # ruamel doesn't natively allow for string output + yaml.dump(in_struct, output) + return output.getvalue().decode("utf-8") + + +class FileLock: + """Basic file locking class that acquires and releases locks. + + Recommended usage is the context manager which will handle everything for you + + with FileLock("basic_file.txt"): + Path("basic_file.txt").write_text("some text") + + If a lock is already in place, FileLock will wait up to seconds + """ + + def __init__(self, file_name, timeout=10): + self.lock = Path(f"{file_name}.lock") + self.timeout = timeout + + def wait_file(self): + """Wait for the lock file to be released, then acquire it.""" + timeout_after = time.time() + self.timeout + while self.lock.exists(): + if time.time() <= timeout_after: + time.sleep(1) + else: + raise exceptions.BrokerError( + f"Timeout while waiting for lock release: {self.lock.absolute()}" + ) + self.lock.touch() + + def return_file(self): + """Release the lock file.""" + self.lock.unlink() + + def __enter__(self): # noqa: D105 + self.wait_file() + + def __exit__(self, *tb_info): # noqa: D105 + self.return_file() + + +@contextmanager +def data_to_tempfile(data, path=None, as_tar=False): + """Write data to a temporary file and return the path.""" + path = Path(path or uuid4().hex[-10]) + logger.debug(f"Creating temporary file {path.absolute()}") + if isinstance(data, bytes): + path.write_bytes(data) + elif isinstance(data, str): + path.write_text(data) + else: + raise TypeError(f"data must be bytes or str, not {type(data)}") + if as_tar: + tar = tarfile.open(path) + yield tarfile.open(path) + tar.close() + else: + yield path + path.unlink() + + +@contextmanager +def temporary_tar(paths): + """Create a temporary tar file and return the path.""" + temp_tar = Path(f"{uuid4().hex[-10]}.tar") + with tarfile.open(temp_tar, mode="w") as tar: + for path in paths: + logger.debug(f"Adding {path.absolute()} to {temp_tar.absolute()}") + tar.add(path, arcname=path.name) + yield temp_tar.absolute() + temp_tar.unlink() diff --git a/broker/helpers/inventory.py b/broker/helpers/inventory.py new file mode 100644 index 00000000..d635fa64 --- /dev/null +++ b/broker/helpers/inventory.py @@ -0,0 +1,160 @@ +"""Inventory management utilities.""" + +import threading + +from ruamel.yaml import YAML + +from broker.helpers.dict_utils import dict_from_paths, merge_dicts +from broker.helpers.file_utils import load_file + +yaml = YAML() +yaml.default_flow_style = False +yaml.sort_keys = False + +INVENTORY_LOCK = threading.Lock() +SPECIAL_INVENTORY_FIELDS = {} # use the _special_inventory_field decorator to add new fields + + +def _special_inventory_field(action_name): + """Register inventory field actions.""" + + def decorator(func): + SPECIAL_INVENTORY_FIELDS[action_name] = func + return func + + return decorator + + +def load_inventory(filter=None): + """Load all local hosts in inventory. + + :param filter: A filter string to apply to the inventory. + + :return: list of dictionaries + """ + from broker.helpers.results import eval_filter + from broker.settings import inventory_path + + inv_data = load_file(inventory_path, warn=False) + if inv_data and filter: + inv_data = eval_filter(inv_data, filter) + return inv_data or [] + + +def update_inventory(add=None, remove=None): + """Update list of local hosts in the checkout interface. + + :param add: list of dictionaries representing new hosts + :param remove: list of strings representing hostnames or names to be removed + + :return: no return value + """ + from broker.settings import inventory_path + + if add and not isinstance(add, list): + add = [add] + elif not add: + add = [] + if remove and not isinstance(remove, list): + remove = [remove] + with INVENTORY_LOCK: + inv_data = load_inventory() + if inv_data: + inventory_path.unlink() + + if remove: + for host in inv_data[::-1]: + if host["hostname"] in remove or host.get("name") in remove: + # iterate through new hosts and update with old host data if it would nullify + for new_host in add: + if host["hostname"] == new_host["hostname"] or host.get( + "name" + ) == new_host.get("name"): + # update missing data in the new_host with the old_host data + new_host.update(merge_dicts(new_host, host)) + inv_data.remove(host) + if add: + inv_data.extend(add) + + inventory_path.touch() + yaml.dump(inv_data, inventory_path) + + +def flip_provider_actions(provider_actions): + """Flip the mapping of actions->provider to provider->actions.""" + flipped = {} + for action, (provider, _) in provider_actions.items(): + provider_name = provider.__name__ + if provider_name not in flipped: + flipped[provider_name] = [] + flipped[provider_name].append(action) + return flipped + + +def inventory_fields_to_dict(inventory_fields, host_dict, **extras): + """Convert a dicionary-like representation of inventory fields to a resolved dictionary. + + inventory fields, as set in the config look like this, in yaml: + inventory_fields: + Host: hostname | name + Provider: _broker_provider + Action: $action + OS: os_distribution os_distribution_version + + We then process that into a dictionary with inventory values like this: + { + "Host": "some.test.host", + "Provider": "AnsibleTower", + "Action": "deploy-rhel", + "OS": "RHEL 8.4" + } + + Notes: The special syntax use in Host and Action fields <$action> is a special keyword that + represents a more complex field resolved by Broker. + Also, the Host field represents a priority order of single values, + so if hostname is not present, name will be used. + Finally, spaces between values are preserved. This lets us combine multiple values in a single field. + """ + return { + name: _resolve_inv_field(field, host_dict, **extras) + for name, field in inventory_fields.items() + } + + +def _resolve_inv_field(field, host_dict, **extras): + """Real functionality for inventory_fields_to_dict, allows recursive evaluation.""" + # Users can specify multiple values to try in order of priority, so evaluate each + if "|" in field: + resolved = [_resolve_inv_field(f.strip(), host_dict, **extras) for f in field.split("|")] + for val in resolved: + if val and val != "Unknown": + return val + return "Unknown" + # Users can combine multiple values in a single field, so evaluate each + if " " in field: + return " ".join(_resolve_inv_field(f, host_dict, **extras) for f in field.split()) + # Some field values require special handling beyond what the existing syntax allows + if special_field_func := SPECIAL_INVENTORY_FIELDS.get(field): + return special_field_func(host_dict, **extras) + # Otherwise, try to get the value from the host dictionary + return dict_from_paths(host_dict, {"_": field}, sep=".")["_"] or "Unknown" + + +@_special_inventory_field("$action") +def get_host_action(host_dict, provider_actions=None, **_): + """Get a more focused set of fields from the host inventory.""" + if not provider_actions: + return "$actionError" + # Flip the mapping of actions->provider to provider->actions + flipped_actions = {} + for action, (provider, _) in provider_actions.items(): + provider_name = provider.__name__ + if provider_name not in flipped_actions: + flipped_actions[provider_name] = [] + flipped_actions[provider_name].append(action) + # Get the host's action, based on its provider + provider = host_dict["_broker_provider"] + for opt in flipped_actions[provider]: + if action := host_dict["_broker_args"].get(opt): + return action + return "Unknown" diff --git a/broker/helpers/misc.py b/broker/helpers/misc.py new file mode 100644 index 00000000..7da3ccf6 --- /dev/null +++ b/broker/helpers/misc.py @@ -0,0 +1,261 @@ +"""Miscellaneous helper functions and classes.""" + +import getpass +import inspect +import json +import logging +import os +from pathlib import Path +import sys +import threading +import time + +import click +from rich.table import Table + +from broker import exceptions +from broker.settings import clone_global_settings + +logger = logging.getLogger(__name__) + + +def resolve_nick(nick, broker_settings=None): + """Check if the nickname exists. Used to define broker arguments. + + :param nick: String representing the name of a nick + :param broker_settings: Optional settings object to use instead of global settings + + :return: a dictionary mapping argument names and values + """ + _settings = broker_settings or clone_global_settings() + nick_names = _settings.get("NICKS") or {} + if nick in nick_names: + return _settings.NICKS[nick].to_dict() + else: + raise exceptions.UserError(f"Unknown nick: {nick}") + + +def kwargs_from_click_ctx(ctx): + """Convert a Click context object to a dictionary of keyword arguments.""" + # if users use `=` to note arg=value assignment, then we need to split it + _args = [] + for arg in ctx.args: + if "=" in arg: + _args.extend(arg.split("=")) + else: + _args.append(arg) + ctx.args = _args + # if additional arguments were passed, include them in the broker args + # strip leading -- characters + return { + (key[2:] if key.startswith("--") else key): val + for key, val in zip(ctx.args[::2], ctx.args[1::2]) + } + + +class Emitter: + """Class that provides a simple interface to emit messages to a json-formatted file. + + This module also has an instance of this class called "emit" that should be used + instead of this class directly. + + Usage examples: + helpers.emit(key=value, another=5) + helpers.emit({"key": "value", "another": 5}) + """ + + EMIT_LOCK = threading.Lock() + + def __init__(self, emit_file=None): + """Can empty init and set the file later.""" + self.file = None + if emit_file: + self.file = self.set_file(emit_file) + + def set_file(self, file_path): + """Set the file to emit to.""" + if file_path: + self.file = Path(file_path) + self.file.parent.mkdir(exist_ok=True, parents=True) + if self.file.exists(): + self.file.unlink() + self.file.touch() + + def emit_to_file(self, *args, **kwargs): + """Emit data to the file, keeping existing data in-place.""" + if not self.file: + return + for arg in args: + if not isinstance(arg, dict): + raise exceptions.BrokerError(f"Received an invalid data emission {arg}") + kwargs.update(arg) + for key, val in kwargs.items(): + if getattr(val, "json", None): + kwargs[key] = val.json + with self.EMIT_LOCK: + curr_data = json.loads(self.file.read_text() or "{}") + curr_data.update(kwargs) + self.file.write_text(json.dumps(curr_data, indent=4, sort_keys=True)) + + def __call__(self, *args, **kwargs): + """Allow emit to be used like a function.""" + return self.emit_to_file(*args, **kwargs) + + +emit = Emitter() + + +def update_log_level(ctx, param, value): + """Update the log level and file logging settings for the Broker. + + Args: + ctx: The Click context object. + param: The Click parameter object. + value: The new log level value. + """ + from broker.logging import setup_logging + + setup_logging(console_level=value) + + +def set_emit_file(ctx, param, value): + """Update the file that the Broker emits data to.""" + emit.set_file(value) + + +def fork_broker(): + """Fork the Broker process to run in the background.""" + pid = os.fork() + if pid: + logger.info(f"Running broker in the background with pid: {pid}") + sys.exit(0) + from broker.logging import setup_logging + + setup_logging(console_level="silent", file_level="silent") + + +def handle_keyboardinterrupt(*args): + """Handle keyboard interrupts gracefully. + + Offer the user a choice between keeping Broker alive in the background, killing it, or resuming execution. + """ + choice = click.prompt( + "\nEnding Broker while running may not end processes being monitored.\n" + "Would you like to switch Broker to run in the Background, Kill it, or Resume execution?\n", + type=click.Choice(["b", "k", "r"]), + default="r", + ).lower() + if choice == "b": + fork_broker() + elif choice == "k": + raise exceptions.BrokerError("Broker killed by user.") + elif choice == "r": + click.echo("Resuming execution...") + + +def translate_timeout(timeout): + """Allow for flexible timeout definitions, converts other units to ms. + + acceptable units are (s)econds, (m)inutes, (h)ours, (d)ays + """ + if isinstance(timeout, str): + timeout, unit = int(timeout[:-1]), timeout[-1] + if unit == "d": + timeout *= 24 + unit = "h" + if unit == "h": + timeout *= 60 + unit = "m" + if unit == "m": + timeout *= 60 + unit = "s" + if unit == "s": + timeout *= 1000 + return timeout if isinstance(timeout, int) else 0 + + +def simple_retry( + cmd, cmd_args=None, cmd_kwargs=None, max_timeout=60, _cur_timeout=1, terminal_exceptions=None +): + """Re(Try) a function given its args and kwargs up until a max timeout.""" + cmd_args = cmd_args if cmd_args else [] + cmd_kwargs = cmd_kwargs if cmd_kwargs else {} + terminal_exceptions = terminal_exceptions or () + + try: + return cmd(*cmd_args, **cmd_kwargs) + except terminal_exceptions: + raise + except Exception as err: + new_wait = _cur_timeout * 2 + if new_wait > max_timeout: + raise err + logger.warning( + f"Tried {cmd=} with {cmd_args=}, {cmd_kwargs=} but received {err=}" + f"\nTrying again in {_cur_timeout} seconds." + ) + time.sleep(_cur_timeout) + simple_retry(cmd, cmd_args, cmd_kwargs, max_timeout, new_wait, terminal_exceptions) + + +def find_origin(): + """Move up the call stack to find tests, fixtures, or cli invocations. + + Additionally, return the jenkins url, if it exists. + """ + prev, _frame, jenkins_url = None, None, os.environ.get("BUILD_URL") + for frame in inspect.stack(): + if frame.function == "checkout" and frame.filename.endswith("broker/commands.py"): + return f"broker_cli:{getpass.getuser()}", jenkins_url + if frame.function.startswith("test_"): + return f"{frame.function}:{frame.filename}", jenkins_url + if frame.function == "call_fixture_func": + # attempt to find the test name from the fixture's request object + if _frame and (request := _frame.frame.f_locals.get("request")): + return f"{prev} for {request.node._nodeid}", jenkins_url + # otherwise, return the fixture name and filename + return prev or "Unknown fixture", jenkins_url + prev, _frame = f"{frame.function}:{frame.filename}", frame + return f"Unknown origin by {getpass.getuser()}", jenkins_url + + +def dictlist_to_table(dict_list, title=None, _id=False, headers=True): + """Convert a list of dictionaries to a rich table.""" + # I like pretty colors, so let's cycle through them + column_colors = ["cyan", "magenta", "green", "yellow", "blue", "red"] + curr_color = 0 + table = Table(title=title) + # construct the columns + if _id: # likely just for inventory tables + table.add_column("Id", justify="left", style=column_colors[curr_color], no_wrap=True) + curr_color += 1 + for key in dict_list[0]: # assume all dicts have the same keys + table.add_column(key, justify="left", style=column_colors[curr_color]) + curr_color += 1 + if curr_color >= len(column_colors): + curr_color = 0 + # add the rows + for id_num, data_dict in enumerate(dict_list): + row = [str(id_num)] if _id else [] + row.extend([str(value) for value in data_dict.values()]) + table.add_row(*row) + if not headers: + table.show_header = False + return table + + +def dict_to_table(in_dict, title=None, headers=None): + """Convert a dictionary into a rich table.""" + # need to normalize the values first + in_dict = {k: str(v) for k, v in in_dict.items()} + table = Table(title=title) + if isinstance(headers, tuple | list): + table.add_column(headers[0], style="cyan") + table.add_column(headers[1], style="magenta") + else: + table.add_column("key", style="cyan") + table.add_column("value", style="magenta") + table.show_header = False + for key, val in in_dict.items(): + table.add_row(key, val) + return table diff --git a/broker/helpers/results.py b/broker/helpers/results.py new file mode 100644 index 00000000..4f3f038c --- /dev/null +++ b/broker/helpers/results.py @@ -0,0 +1,140 @@ +"""Result and testing helper classes.""" + +from collections import UserDict, namedtuple +from collections.abc import Hashable + +FilterTest = namedtuple("FilterTest", "haystack needle test") + + +class MockStub(UserDict): + """Test helper class. Allows for both arbitrary mocking and stubbing.""" + + def __init__(self, in_dict=None): + """Initialize the class and all nested dictionaries.""" + if in_dict is None: + in_dict = {} + for key, value in in_dict.items(): + if isinstance(value, dict): + setattr(self, key, MockStub(value)) + elif type(value) in (list, tuple): + setattr( + self, + key, + [MockStub(x) if isinstance(x, dict) else x for x in value], + ) + else: + setattr(self, key, value) + super().__init__(in_dict) + + def __getattr__(self, name): + """Fallback to returning self if attribute doesn't exist.""" + return self + + def __getitem__(self, key): + """Get an item from the dictionary-like object. + + If the key is a string, this method will attempt to get an attribute with that name. + If the key is not found, this method will return the object itself. + """ + try: + return super().__getitem__(key) + except KeyError: + if isinstance(key, str): + return getattr(self, key, self) + return self + + def __call__(self, *args, **kwargs): + """Allow MockStub to be used like a function.""" + return self + + def __eq__(self, other): + """Check equality with another object.""" + if isinstance(other, MockStub): + return self.data == other.data + if isinstance(other, dict): + return self.data == other + return False + + def __hash__(self): + """Return a hash value for the object. + + The hash value is computed using the hash value of all hashable attributes of the object. + """ + return hash(tuple(kp for kp in self.__dict__.items() if isinstance(kp[1], Hashable))) + + +def eval_filter(filter_list, raw_filter, filter_key="inv"): + """Run each filter through an eval to get the results.""" + filter_list = [MockStub(item) if isinstance(item, dict) else item for item in filter_list] + for raw_f in raw_filter.split("|"): + if f"@{filter_key}[" in raw_f: + # perform a list filter on the inventory + filter_list = eval( # noqa: S307 + raw_f.replace(f"@{filter_key}", filter_key), {filter_key: filter_list} + ) + filter_list = filter_list if isinstance(filter_list, list) else [filter_list] + elif f"@{filter_key}" in raw_f: + # perform an attribute filter on each host + filter_list = list( + filter( + lambda item: eval( # noqa: S307 + raw_f.replace(f"@{filter_key}", filter_key), {filter_key: item} + ), + filter_list, + ) + ) + return [dict(item) if isinstance(item, MockStub) else item for item in filter_list] + + +class Result: + """Dummy result class for presenting results in dot access.""" + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + """Return a string representation of the object.""" + return f"stdout:\n{self.stdout}\nstderr:\n{self.stderr}\nstatus: {self.status}" + + @classmethod + def from_ssh(cls, stdout, channel): + """Create a Result object from an SSH channel.""" + return cls( + stdout=stdout, + status=channel.get_exit_status(), + stderr=channel.read_stderr()[1].decode("utf-8"), + ) + + @classmethod + def from_duplexed_exec(cls, duplex_exec, runtime=None): + """Create a Result object from a duplexed exec object from podman or docker.""" + if runtime == "podman": + status, (stdout, stderr) = duplex_exec + return cls( + status=status, + stdout=stdout.decode("utf-8") if stdout else "", + stderr=stderr.decode("utf-8") if stderr else "", + ) + + if duplex_exec.output[0]: + stdout = duplex_exec.output[0].decode("utf-8") + else: + stdout = "" + if duplex_exec.output[1]: + stderr = duplex_exec.output[1].decode("utf-8") + else: + stderr = "" + return cls( + status=duplex_exec.exit_code, + stdout=stdout, + stderr=stderr, + ) + + @classmethod + def from_nonduplexed_exec(cls, nonduplex_exec): + """Create a Result object from a nonduplexed exec object from the docker library.""" + return cls( + status=nonduplex_exec.exit_code, + stdout=nonduplex_exec.output.decode("utf-8"), + stderr="", + ) From d049aebff9e7b8357f74c3b11b7ba1b0deeb2f8a Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Thu, 4 Dec 2025 11:31:46 -0500 Subject: [PATCH 03/10] Add a basic shell This adds a very basic "broker shell" command that enters a basic interactive shell. This can be expanded in the future. --- broker/commands.py | 67 +++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/broker/commands.py b/broker/commands.py index 3997b7b3..61cae983 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -11,6 +11,7 @@ setup_logging(console_level=logging.INFO) # Basic setup until settings are loaded +from click_shell import shell from rich.console import Console from rich.syntax import Syntax from rich.table import Table @@ -40,7 +41,7 @@ click.rich_click.COMMAND_GROUPS = { "broker": [ {"name": "Core Actions", "commands": ["checkout", "checkin", "inventory"]}, - {"name": "Extras", "commands": ["execute", "extend", "providers", "config"]}, + {"name": "Extras", "commands": ["execute", "extend", "providers", "config", "shell"]}, ] } @@ -564,3 +565,67 @@ def validate(chunk): logger.info("Validation passed!") except exceptions.BrokerError as err: logger.warning(f"Validation failed: {err}") + + +def _make_shell_help_func(cmd, shell_instance): + """Create a help function that invokes the command with --help. + + This works around a compatibility issue between click_shell and rich_click where + the shell's built-in help system uses a standard HelpFormatter that lacks + rich_click's config attribute. + """ + + def help_func(): + # Invoke the command with --help which properly uses rich_click formatting + try: + cmd.main(["--help"], standalone_mode=False, parent=shell_instance.ctx) + except SystemExit: + pass + + help_func.__name__ = f"help_{cmd.name}" + return help_func + + +@shell( + prompt="broker > ", + intro="Welcome to Broker's interactive shell.\nType 'help' for commands, 'exit' or 'quit' to leave.", +) +def broker_shell(): + """Start an interactive Broker shell session.""" + pass + + +# Register commands to the shell +broker_shell.add_command(checkout) +broker_shell.add_command(checkin) +broker_shell.add_command(inventory) +broker_shell.add_command(execute) +broker_shell.add_command(providers) +broker_shell.add_command(config) + + +# Shell-only commands (not available as normal sub-commands) +@broker_shell.command(name="reload_config") +def reload_config_cmd(): + """Reload Broker's configuration from disk. + + This clears the cached settings, forcing them to be re-read + from the settings file on next access. + """ + settings.settings._settings = None + CONSOLE.print("Configuration cache cleared. Settings will reload on next access.") + + +# Patch help functions on the shell instance to work around click_shell/rich_click incompatibility +for cmd_name, cmd in broker_shell.commands.items(): + setattr(broker_shell.shell, f"help_{cmd_name}", _make_shell_help_func(cmd, broker_shell.shell)) + + +@cli.command(name="shell") +def shell_cmd(): + """Start an interactive Broker shell session. + + This provides a REPL-like interface for running Broker commands + without needing to prefix each with 'broker'. + """ + broker_shell(standalone_mode=False, args=[]) diff --git a/pyproject.toml b/pyproject.toml index c7baa956..21ac4d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "rich", "rich_click", "ruamel.yaml", + "click-shell", ] [project.urls] From becee6eea77e446316464f195a96f60993987683 Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sat, 6 Dec 2025 08:57:46 -0500 Subject: [PATCH 04/10] Switch from click to rich progress bars This switch is pretty straight forward and continues the trend of beautifying the cli. --- broker/commands.py | 13 +++++++++---- broker/providers/ansible_tower.py | 7 +++++-- broker/providers/beaker.py | 7 +++++-- broker/providers/container.py | 5 +++-- broker/providers/foreman.py | 7 +++++-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/broker/commands.py b/broker/commands.py index 61cae983..b7617a2e 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -1,5 +1,6 @@ """Defines the CLI commands for Broker.""" +import contextlib from functools import wraps import logging import signal @@ -577,10 +578,8 @@ def _make_shell_help_func(cmd, shell_instance): def help_func(): # Invoke the command with --help which properly uses rich_click formatting - try: + with contextlib.suppress(SystemExit): cmd.main(["--help"], standalone_mode=False, parent=shell_instance.ctx) - except SystemExit: - pass help_func.__name__ = f"help_{cmd.name}" return help_func @@ -613,7 +612,13 @@ def reload_config_cmd(): from the settings file on next access. """ settings.settings._settings = None - CONSOLE.print("Configuration cache cleared. Settings will reload on next access.") + setup_logging( + console_level=settings.settings.logging.console_level, + file_level=settings.settings.logging.file_level, + log_path=settings.settings.logging.log_path, + structured=settings.settings.logging.structured, + ) + CONSOLE.print("Configuration reloaded.") # Patch help functions on the shell instance to work around click_shell/rich_click incompatibility diff --git a/broker/providers/ansible_tower.py b/broker/providers/ansible_tower.py index 7f64ffc6..ad11b986 100644 --- a/broker/providers/ansible_tower.py +++ b/broker/providers/ansible_tower.py @@ -18,6 +18,7 @@ from packaging.version import InvalidVersion, Version from requests.exceptions import ConnectionError from rich.console import Console +from rich.progress import track from rich.prompt import Prompt from ruamel.yaml import YAML, YAMLError @@ -903,8 +904,10 @@ def get_inventory(self, user=None): for inv in invs: inv_hosts = inv.get_related("hosts", page_size=200).results hosts.extend(inv_hosts) - with click.progressbar(hosts, label="Compiling host information") as hosts_bar: - compiled_host_info = [self._compile_host_info(host) for host in hosts_bar] + compiled_host_info = [ + self._compile_host_info(host) + for host in track(hosts, description="Compiling host information") + ] return compiled_host_info def extend(self, target_vm, new_expire_time=None, provider_labels=None): diff --git a/broker/providers/beaker.py b/broker/providers/beaker.py index f0d413ce..a461827a 100644 --- a/broker/providers/beaker.py +++ b/broker/providers/beaker.py @@ -5,6 +5,7 @@ import click from dynaconf import Validator +from rich.progress import track logger = logging.getLogger(__name__) @@ -158,6 +159,8 @@ def extend(self, host_name, extend_duration=99): def get_inventory(self, *args): """Get a list of hosts and their information from Beaker.""" hosts = self.runtime.user_systems() - with click.progressbar(hosts, label="Compiling host information") as hosts_bar: - compiled_host_info = [self._compile_host_info(host) for host in hosts_bar] + compiled_host_info = [ + self._compile_host_info(host) + for host in track(hosts, description="Compiling host information") + ] return compiled_host_info diff --git a/broker/providers/container.py b/broker/providers/container.py index 86d61559..60fbba7f 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -8,6 +8,7 @@ import click from dynaconf import Validator +from rich.progress import track logger = logging.getLogger(__name__) @@ -261,10 +262,10 @@ def provider_help( def get_inventory(self, name_prefix): """Get all containers that have a matching name prefix.""" name_prefix = name_prefix or self._name_prefix + containers = [cont for cont in self.runtime.containers if cont.name.startswith(name_prefix)] return [ container_info(cont) - for cont in self.runtime.containers - if cont.name.startswith(name_prefix) + for cont in track(containers, description="Compiling host information") ] def extend(self): diff --git a/broker/providers/foreman.py b/broker/providers/foreman.py index 391fc8a4..13ddf85c 100644 --- a/broker/providers/foreman.py +++ b/broker/providers/foreman.py @@ -6,6 +6,7 @@ import click from dynaconf import Validator +from rich.progress import track logger = logging.getLogger(__name__) @@ -195,8 +196,10 @@ def _compile_host_info(self, host): def get_inventory(self, *args, **kwargs): """Synchronize list of hosts on Foreman using set prefix.""" all_hosts = self.runtime.hosts() - with click.progressbar(all_hosts, label="Compiling host information") as hosts_bar: - compiled_host_info = [self._compile_host_info(host) for host in hosts_bar] + compiled_host_info = [ + self._compile_host_info(host) + for host in track(all_hosts, description="Compiling host information") + ] return compiled_host_info def _host_release(self): From 27c7dc995594e38ce09338a56329a8ac4ad3fa1d Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sat, 6 Dec 2025 11:12:26 -0500 Subject: [PATCH 05/10] feat: treat uv.lock as binary and configure merge driver This marks uv.lock as a binary file to suppress diffs and configures a custom merge driver 'uv-lock' to automatically regenerate the lockfile on conflicts. Instructions for configuring the local git merge driver have been added to README.md. --- .gitattributes | 1 + README.md | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..08f8d281 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +uv.lock binary merge=uv-lock diff --git a/README.md b/README.md index 4137bf0c..3bbc4e27 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,15 @@ Copy the example settings file to `broker_settings.yaml` and edit it. To run Broker outside of its base directory, specify the directory with the `BROKER_DIRECTORY` environment variable. +### Handling uv.lock conflicts + +This project treats `uv.lock` as a binary file to suppress large diffs. To automatically resolve conflicts in this file, you can configure a custom merge driver: + +```bash +git config merge.uv-lock.name "Generate uv.lock" +git config merge.uv-lock.driver "uv lock" +``` + # API Usage TODO: Flesh this out From ebd8ff06013f47f16eea85b1acd33872aeaf15b8 Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sat, 6 Dec 2025 10:33:13 -0500 Subject: [PATCH 06/10] Enhance provider help output with Rich formatting Features: - Beaker provider help: - Display individual job XML with syntax highlighting using `rich.syntax.Syntax`. - Format lists of available jobs into `rich` tables. - Container provider help: - Present detailed image information in `rich` tables. - Display image configuration as syntax-highlighted YAML. - Convert lists of available host and app images into `rich` tables. - Add support for the `container_app` parameter to retrieve details for a specific container application. - Foreman provider help: - Render lists of hostgroups and individual hostgroup details in `rich` tables. Refactoring: - Replaced direct `logging.info` calls for user-facing output in `provider_help` methods with `rich.console.Console` for improved presentation. - Removed the `results_limit` parameter, as `rich` tables manage display and filtering more effectively. Configuration: - Integrated `_settings.less_colors` to control color output in the `rich` console, allowing users to disable colors. --- broker/providers/beaker.py | 21 +++++++++--- broker/providers/container.py | 64 +++++++++++++++++++++++++++++------ broker/providers/foreman.py | 55 ++++++++++++++++++++++-------- 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/broker/providers/beaker.py b/broker/providers/beaker.py index a461827a..95b1905b 100644 --- a/broker/providers/beaker.py +++ b/broker/providers/beaker.py @@ -5,7 +5,9 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track +from rich.syntax import Syntax logger = logging.getLogger(__name__) @@ -129,17 +131,28 @@ def submit_job(self, max_wait=None, **kwargs): def provider_help(self, jobs=False, job=None, **kwargs): """Print useful information from the Beaker provider.""" - results_limit = kwargs.get("results_limit", self._settings.container.results_limit) + rich_console = Console(no_color=self._settings.less_colors) if job: if not job.startswith("J:"): job = f"J:{job}" - logger.info(self.runtime.job_clone(job, prettyxml=True, dryrun=True).stdout) + job_xml = self.runtime.job_clone(job, prettyxml=True, dryrun=True).stdout + syntax = Syntax(job_xml, "xml", theme="monokai", line_numbers=True) + rich_console.print(syntax) elif jobs: result = self.runtime.job_list(**kwargs).stdout.splitlines() if res_filter := kwargs.get("results_filter"): result = helpers.eval_filter(result, res_filter, "res") - result = "\n".join(result[:results_limit]) - logger.info(f"Available jobs:\n{result}") + result = result if isinstance(result, list) else [result] + if not result: + logger.warning("No jobs found!") + return + job_table = helpers.dictlist_to_table( + [{"name": j} for j in result], + title="Available Jobs", + _id=False, + headers=False, + ) + rich_console.print(job_table) def release(self, host_name, job_id): """Release a hosts reserved from Beaker by cancelling the job.""" diff --git a/broker/providers/container.py b/broker/providers/container.py index 60fbba7f..d3779d5c 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -8,7 +8,9 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track +from rich.syntax import Syntax logger = logging.getLogger(__name__) @@ -231,15 +233,41 @@ def construct_host(self, provider_params, host_classes, **kwargs): return host_inst def provider_help( - self, container_hosts=False, container_host=None, container_apps=False, **kwargs + self, + container_hosts=False, + container_host=None, + container_apps=False, + container_app=None, + **kwargs, ): """Return useful information about container images.""" - results_limit = kwargs.get("results_limit", self._settings.Container.results_limit) - if container_host: - logger.info( - f"Information for {container_host} container-host:\n" - f"{helpers.yaml_format(self.runtime.image_info(container_host))}" + rich_console = Console(no_color=self._settings.less_colors) + if container_host or container_app: + image_name = container_host or container_app + image_info = self.runtime.image_info(image_name) + if not image_info: + logger.warning(f"Image {image_name} not found!") + return + # Extract config separately for special formatting + config = image_info.pop("config", {}) + # Display basic info in a table + info_table = helpers.dict_to_table( + image_info, + title=f"{image_name} Information", ) + rich_console.print(info_table) + # Display config as syntax-highlighted YAML + if config: + config_yaml = helpers.yaml_format(config) + syntax = Syntax( + config_yaml, + "yaml", + theme="monokai", + line_numbers=False, + background_color="default", + ) + rich_console.print("\n[bold]Image Configuration[/bold]") + rich_console.print(syntax) elif container_hosts: images = [ img.tags[0] @@ -249,15 +277,31 @@ def provider_help( if res_filter := kwargs.get("results_filter"): images = helpers.eval_filter(images, res_filter, "res") images = images if isinstance(images, list) else [images] - images = "\n".join(images[:results_limit]) - logger.info(f"Available host images:\n{images}") + if not images: + logger.warning("No host images found!") + return + image_table = helpers.dictlist_to_table( + [{"name": img} for img in images], + title="Available Host Images", + _id=False, + headers=False, + ) + rich_console.print(image_table) elif container_apps: images = [img.tags[0] for img in self.runtime.images if img.tags] if res_filter := kwargs.get("results_filter"): images = helpers.eval_filter(images, res_filter, "res") images = images if isinstance(images, list) else [images] - images = "\n".join(images[:results_limit]) - logger.info(f"Available app images:\n{images}") + if not images: + logger.warning("No app images found!") + return + image_table = helpers.dictlist_to_table( + [{"name": img} for img in images], + title="Available App Images", + _id=False, + headers=False, + ) + rich_console.print(image_table) def get_inventory(self, name_prefix): """Get all containers that have a matching name prefix.""" diff --git a/broker/providers/foreman.py b/broker/providers/foreman.py index 13ddf85c..dbb6d731 100644 --- a/broker/providers/foreman.py +++ b/broker/providers/foreman.py @@ -6,10 +6,12 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track logger = logging.getLogger(__name__) +from broker import helpers from broker.binds import foreman from broker.helpers import Result from broker.providers import Provider @@ -161,27 +163,50 @@ def provider_help( **kwargs, ): """Return useful information about Foreman provider.""" + rich_console = Console(no_color=self._settings.less_colors) if hostgroups: all_hostgroups = self.runtime.hostgroups() - logger.info(f"On Foreman {self.instance} you have the following hostgroups:") - for hg in all_hostgroups["results"]: - logger.info(f"- {hg['title']}") - elif hostgroup: - logger.info( - f"On Foreman {self.instance} the hostgroup {hostgroup} has the following properties:" + if not all_hostgroups.get("results"): + logger.warning("No hostgroups found!") + return + hostgroup_names = [hg["title"] for hg in all_hostgroups["results"]] + if res_filter := kwargs.get("results_filter"): + hostgroup_names = helpers.eval_filter(hostgroup_names, res_filter, "res") + hostgroup_names = ( + hostgroup_names if isinstance(hostgroup_names, list) else [hostgroup_names] + ) + if not hostgroup_names: + logger.warning("No hostgroups found!") + return + hostgroup_table = helpers.dictlist_to_table( + [{"name": hg} for hg in hostgroup_names], + title=f"Available Hostgroups on {self.instance}", + _id=False, + headers=False, ) + rich_console.print(hostgroup_table) + elif hostgroup: data = self.runtime.hostgroup(name=hostgroup) + if not data: + logger.warning(f"Hostgroup {hostgroup} not found!") + return fields_of_interest = { - "description": "description", - "operating_system": "operatingsystem_name", - "domain": "domain_name", - "subnet": "subnet_name", - "subnet6": "subnet6_name", + "Description": "description", + "Operating System": "operatingsystem_name", + "Domain": "domain_name", + "Subnet": "subnet_name", + "Subnet6": "subnet6_name", } - for name, field in fields_of_interest.items(): - value = data.get(field, False) - if value: - logger.info(f" {name}: {value}") + display_data = { + name: data.get(field, "N/A") + for name, field in fields_of_interest.items() + if data.get(field) + } + hostgroup_table = helpers.dict_to_table( + display_data, + title=f"{hostgroup} Information", + ) + rich_console.print(hostgroup_table) def _compile_host_info(self, host): return { From b5b5e49bb6de744178155443f0f90ff99087159e Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Wed, 26 Nov 2025 20:03:00 -0500 Subject: [PATCH 07/10] Add 'scenarios' CLI for managing chained Broker actions Features: - Introduce the new 'scenarios' feature, enabling the definition and execution of complex workflows as chained Broker actions within YAML files. - Add a new `broker scenarios` CLI group to manage these workflows: - `list`: Displays all available scenario files found in the scenarios directory. - `execute `: Runs a specified scenario, supporting command-line variable and configuration overrides, and background execution. - `info `: Provides a summary of a scenario's configuration, variables, and steps. - `validate `: Checks a scenario file against its defined JSON schema for structural correctness. - Scenario functionality includes: - Jinja2 templating for dynamic values within scenario definitions. - Conditional step execution using `when` clauses. - Iterative step execution via `loop` constructs over iterables or inventory filters. - Capture of step outputs into new scenario variables for subsequent steps. - Configurable error handling with `on_error` blocks and `exit_on_error` flags. - Management of a scenario-specific inventory for `checkout` and `checkin` actions. - Support for nesting and running other scenarios as steps. - Implement `ScenarioRunner` as the core class for loading, validating, and executing scenario definitions. - Add `ScenarioError` to handle exceptions specific to scenario execution. Configuration: - Introduce `broker/scenario_schema.json` to formally define and validate the structure of scenario YAML files. - Update `pyproject.toml` to include `paramiko` as a recognized SSH session and interactive shell entry point. --- broker/commands.py | 119 ++++- broker/exceptions.py | 6 + broker/scenario_schema.json | 149 +++++++ broker/scenarios.py | 839 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1112 insertions(+), 1 deletion(-) create mode 100644 broker/scenario_schema.json create mode 100644 broker/scenarios.py diff --git a/broker/commands.py b/broker/commands.py index b7617a2e..f1767b2f 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -42,7 +42,7 @@ click.rich_click.COMMAND_GROUPS = { "broker": [ {"name": "Core Actions", "commands": ["checkout", "checkin", "inventory"]}, - {"name": "Extras", "commands": ["execute", "extend", "providers", "config", "shell"]}, + {"name": "Extras", "commands": ["execute", "extend", "providers", "config", "scenarios", "shell"]}, ] } @@ -634,3 +634,120 @@ def shell_cmd(): without needing to prefix each with 'broker'. """ broker_shell(standalone_mode=False, args=[]) +# --- Scenarios CLI Group --- + + +@cli.group() +def scenarios(): + """Manage and execute Broker scenarios. + + Scenarios allow you to chain multiple Broker actions together in a YAML file. + """ + pass + + +@guarded_command(group=scenarios, name="list") +def scenarios_list(): + """List all available scenarios in the scenarios directory.""" + from broker.scenarios import SCENARIOS_DIR, list_scenarios + + scenario_names = list_scenarios() + if not scenario_names: + CONSOLE.print(f"No scenarios found in {SCENARIOS_DIR}") + return + + table = Table(title="Available Scenarios") + table.add_column("Name", style="cyan") + table.add_column("Path", style="magenta") + + for name in scenario_names: + table.add_row(name, str(SCENARIOS_DIR / f"{name}.yaml")) + + CONSOLE.print(table) + + +@guarded_command( + group=scenarios, + name="execute", + context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, +) +@click.argument("scenario", type=str) +@click.option("-b", "--background", is_flag=True, help="Run scenario in the background") +@click.pass_context +def scenarios_execute(ctx, scenario, background): + """Execute a scenario file. + + SCENARIO can be a name (found in scenarios dir) or a path to a YAML file. + + Additional arguments are passed as variable overrides: + + broker scenarios execute my_scenario --MY_VAR value --ANOTHER_VAR value + + Config overrides use dotted notation: + + broker scenarios execute my_scenario --config.settings.ssh.backend paramiko + """ + from broker.scenarios import ScenarioRunner, find_scenario + + # Parse CLI args into variables and config overrides + cli_vars = {} + cli_config = {} + extra_args = helpers.kwargs_from_click_ctx(ctx) + + for key, val in extra_args.items(): + if key.startswith("config."): + cli_config[key] = val + else: + cli_vars[key] = val + + if background: + helpers.fork_broker() + + scenario_path = find_scenario(scenario) + runner = ScenarioRunner( + scenario_path=scenario_path, + cli_vars=cli_vars, + cli_config=cli_config, + ) + runner.run() + + +@guarded_command(group=scenarios) +@click.argument("scenario", type=str) +@click.option("--no-syntax", is_flag=True, help="Disable syntax highlighting") +def info(scenario, no_syntax): + """Get information about a scenario. + + Displays the scenario's config, variables, and step names. + """ + from broker.scenarios import ScenarioRunner, find_scenario + + scenario_path = find_scenario(scenario) + runner = ScenarioRunner(scenario_path=scenario_path) + info_data = runner.get_info() + + output = helpers.yaml_format(info_data) + if no_syntax: + CONSOLE.print(output) + else: + CONSOLE.print(Syntax(output, "yaml", background_color="default")) + + +@guarded_command(group=scenarios, name="validate") +@click.argument("scenario", type=str) +def scenarios_validate(scenario): + """Validate a scenario file against the schema. + + Checks for syntax errors and schema violations. + """ + from broker.scenarios import find_scenario, validate_scenario + + scenario_path = find_scenario(scenario) + is_valid, error_msg = validate_scenario(scenario_path) + + if is_valid: + logger.info(f"Scenario '{scenario}' is valid!") + if error_msg: # Schema not found message + logger.warning(error_msg) + else: + logger.error(f"Scenario '{scenario}' is invalid: {error_msg}") diff --git a/broker/exceptions.py b/broker/exceptions.py index e33d05e8..102962ce 100644 --- a/broker/exceptions.py +++ b/broker/exceptions.py @@ -91,3 +91,9 @@ class ParamikoBindError(BrokerError): """Raised when a problem occurs at the Paramiko bind level.""" error_code = 15 + + +class ScenarioError(BrokerError): + """Raised when a problem occurs during scenario execution.""" + + error_code = 16 diff --git a/broker/scenario_schema.json b/broker/scenario_schema.json new file mode 100644 index 00000000..96752138 --- /dev/null +++ b/broker/scenario_schema.json @@ -0,0 +1,149 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Scenario Schema", + "description": "A schema for validating scenario YAML files based on the defined specification.", + "type": "object", + "properties": { + "config": { + "type": "object", + "description": "Global configuration settings for the scenario execution.", + "properties": { + "inventory_path": { + "type": "string", + "description": "The file path to the scenario's dedicated inventory file." + }, + "settings": { + "type": "object", + "description": "A nested map of provider-specific settings.", + "additionalProperties": { + "type": "object" + } + } + }, + "additionalProperties": false + }, + "variables": { + "type": "object", + "description": "A key-value map of variables to be used within the steps.", + "additionalProperties": { + "type": ["string", "number", "boolean", "array", "object"] + } + }, + "steps": { + "type": "array", + "description": "A list of step objects that define the scenario's actions.", + "items": { + "$ref": "#/definitions/step" + } + } + }, + "required": [ + "steps" + ], + "additionalProperties": false, + "definitions": { + "step": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A human-readable name for the step." + }, + "action": { + "type": "string", + "description": "The type of action to perform.", + "enum": [ + "checkout", + "checkin", + "inventory", + "ssh", + "scp", + "sftp", + "execute", + "run_scenarios", + "exit" + ] + }, + "arguments": { + "type": "object", + "description": "An optional key-value map of arguments specific to the chosen action." + }, + "with": { + "type": "object", + "description": "Specifies the target hosts for the action.", + "properties": { + "hosts": { + "type": "string", + "description": "Can be 'scenario_inventory' or an inventory filter expression." + } + }, + "required": ["hosts"], + "additionalProperties": false + }, + "when": { + "type": "string", + "description": "A conditional expression that must evaluate to true for the step to run." + }, + "loop": { + "type": "object", + "description": "Defines a loop to run the action multiple times.", + "properties": { + "iterable": { + "type": "string", + "description": "An inventory filter or a variable that resolves to a list." + }, + "iter_var": { + "type": "string", + "description": "The name of the variable to hold the current item during each loop iteration." + }, + "on_error": { + "type": "string", + "description": "Defines behavior on loop item failure.", + "enum": ["continue"] + } + }, + "required": ["iterable", "iter_var"], + "additionalProperties": false + }, + "capture": { + "type": "object", + "description": "Captures the output of the step into a variable.", + "properties": { + "as": { + "type": "string", + "description": "The name of the new variable to store the result in." + }, + "transform": { + "type": "string", + "description": "A templating expression to transform the step.output before saving it." + } + }, + "required": ["as"], + "additionalProperties": false + }, + "on_error": { + "type": "array", + "description": "A list of nested steps to execute if the current step fails.", + "items": { + "$ref": "#/definitions/step" + } + }, + "exit_on_error": { + "type": "boolean", + "description": "If False, the scenario will continue even if the step fails and no on_error block is defined.", + "default": true + }, + "parallel": { + "type": "boolean", + "description": "If False, forces the action to run sequentially when targeting multiple hosts.", + "default": true + } + }, + "required": [ + "name", + "action" + ], + "additionalProperties": false + } + } +} diff --git a/broker/scenarios.py b/broker/scenarios.py new file mode 100644 index 00000000..31a46b7c --- /dev/null +++ b/broker/scenarios.py @@ -0,0 +1,839 @@ +"""Broker Scenarios module for chaining multiple Broker actions together. + +This module provides functionality to execute scenario files that define +a sequence of Broker-based actions (checkout, checkin, execute, ssh, etc.) +with support for templating, looping, error handling, and variable capture. + +Usage: + runner = ScenarioRunner("/path/to/scenario.yaml", cli_vars={"MY_VAR": "value"}) + runner.run() +""" + +import json +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path + +import jinja2 +import jsonschema +from ruamel.yaml import YAML + +from broker import helpers +from broker.broker import Broker +from broker.exceptions import ScenarioError +from broker.settings import BROKER_DIRECTORY, create_settings + +logger = logging.getLogger(__name__) + +yaml = YAML() +yaml.default_flow_style = False +yaml.sort_keys = False + +# Directory where scenarios are stored by default +SCENARIOS_DIR = BROKER_DIRECTORY / "scenarios" +SCENARIOS_DIR.mkdir(parents=True, exist_ok=True) + +# Load the schema from the package +SCHEMA_PATH = Path(__file__).parent / "scenario_schema.json" + + +def get_schema(): + """Load and return the scenario JSON schema.""" + if SCHEMA_PATH.exists(): + with SCHEMA_PATH.open() as f: + return json.load(f) + return None + + +def find_scenario(name_or_path): + """Find a scenario file by name or path. + + Args: + name_or_path: Either a scenario name (without extension) or a full path + + Returns: + Path object to the scenario file + + Raises: + ScenarioError: If the scenario cannot be found + """ + # First, check if it's a direct path + path = Path(name_or_path) + if path.exists() and path.is_file(): + return path + + # Add .yaml extension if not present + if not path.suffix: + path = path.with_suffix(".yaml") + if path.exists(): + return path + + # Check in the scenarios directory + scenario_path = SCENARIOS_DIR / path.name + if scenario_path.exists(): + return scenario_path + + # Check with .yaml extension + scenario_path = SCENARIOS_DIR / f"{name_or_path}.yaml" + if scenario_path.exists(): + return scenario_path + + # Check with .yml extension + scenario_path = SCENARIOS_DIR / f"{name_or_path}.yml" + if scenario_path.exists(): + return scenario_path + + raise ScenarioError(f"Scenario not found: {name_or_path}") + + +def list_scenarios(): + """List all scenarios in the scenarios directory. + + Returns: + List of scenario names (without extensions) + """ + if not SCENARIOS_DIR.exists(): + return [] + + scenarios = [] + for file in SCENARIOS_DIR.iterdir(): + if file.suffix in (".yaml", ".yml"): + scenarios.append(file.stem) + return sorted(scenarios) + + +def render_template(template_str, context): + """Render a Jinja2 template string using the provided context. + + If the input is not a string or doesn't contain template syntax, + it is returned as-is. + + Args: + template_str: String that may contain Jinja2 template syntax + context: Dictionary of variables for template rendering + + Returns: + Rendered string or original value if not a template + """ + if not isinstance(template_str, str): + return template_str + + # Check if it looks like a template + if "{{" not in template_str and "{%" not in template_str: + return template_str + + try: + env = jinja2.Environment(undefined=jinja2.StrictUndefined) + template = env.from_string(template_str) + return template.render(**context) + except jinja2.UndefinedError as e: + logger.warning(f"Template rendering warning: {e}") + raise ScenarioError(f"Undefined variable in template: {e}") from e + + +def evaluate_condition(expression, context): + """Evaluate a Jinja2 expression returning a boolean. + + Used for 'when' conditions in steps. + + Args: + expression: A string expression that should evaluate to True/False + context: Dictionary of variables for expression evaluation + + Returns: + Boolean result of the expression + """ + if not expression: + return True + + # Wrap in {{ }} if not already a Jinja2 expression + if not expression.strip().startswith("{{"): + expression = f"{{{{ {expression} }}}}" + + result = render_template(expression, context) + + # Convert string result to boolean + if isinstance(result, bool): + return result + if str(result).lower() in ("true", "yes", "1"): + return True + if str(result).lower() in ("false", "no", "0", "none", ""): + return False + return bool(result) + + +def recursive_render(data, context): + """Recursively render template strings in a dictionary or list. + + Args: + data: Dictionary, list, or value that may contain template strings + context: Dictionary of variables for template rendering + + Returns: + Data structure with all template strings rendered + """ + if isinstance(data, dict): + return {k: recursive_render(v, context) for k, v in data.items()} + elif isinstance(data, list): + return [recursive_render(item, context) for item in data] + elif isinstance(data, str): + return render_template(data, context) + else: + return data + + +def resolve_hosts_reference(hosts_ref, scenario_inventory, context): + """Resolve a hosts reference to a list of host objects. + + Args: + hosts_ref: Either 'scenario_inventory' or an inventory filter expression + scenario_inventory: List of hosts checked out by this scenario + context: Template context for rendering + + Returns: + List of host objects + """ + if hosts_ref == "scenario_inventory": + return scenario_inventory.copy() + + # Check if it's a filter expression (contains @inv) + if "@inv" in hosts_ref: + # Filter against the scenario inventory using Broker's filter + return helpers.eval_filter(scenario_inventory, hosts_ref, filter_key="inv") + + # Try to render as a template and see if it's a variable + rendered = render_template(hosts_ref, context) + if isinstance(rendered, list): + return rendered + + return [] + + +class StepMemory: + """Memory storage for a single step's execution state.""" + + def __init__(self, name): + self.name = name + self.output = None + self.status = "pending" + self._broker_inst = None + self._error = None + + def to_dict(self): + """Convert step memory to a dictionary for template access.""" + return { + "name": self.name, + "output": self.output, + "status": self.status, + } + + def __getitem__(self, key): + """Allow dictionary-style access for template compatibility.""" + return getattr(self, key, None) + + def get(self, key, default=None): + """Get attribute with default value.""" + return getattr(self, key, default) + + +class ScenarioRunner: + """Main class for loading, validating, and executing scenario files. + + A ScenarioRunner handles the complete lifecycle of a scenario: + - Loading and validating the YAML file + - Setting up configuration and variables + - Executing steps in sequence + - Managing the scenario-specific inventory + - Handling errors and cleanup + """ + + def __init__(self, scenario_path, cli_vars=None, cli_config=None): + """Initialize the ScenarioRunner. + + Args: + scenario_path: Path to the scenario YAML file + cli_vars: Dictionary of variables passed via CLI that override scenario variables + cli_config: Dictionary of config overrides passed via CLI (e.g., config.settings.X) + """ + self.scenario_path = Path(scenario_path) + self.scenario_name = self.scenario_path.stem + self.cli_vars = cli_vars or {} + self.cli_config = cli_config or {} + + # Load and validate the scenario file + self.data = self._load_and_validate() + + # Initialize configuration + self.config = self.data.get("config", {}) + self._apply_cli_config_overrides() + + # Create settings object with scenario config merged + user_settings = self.config.get("settings", {}) + self._settings = create_settings(config_dict=user_settings, skip_validation=True) + + # Initialize variables (scenario vars, then CLI overrides on top) + self.variables = self.data.get("variables", {}).copy() + self.variables.update(self.cli_vars) + + # Steps memory: mapping step name -> StepMemory + self.steps_memory = {} + + # Scenario inventory: hosts checked out by this scenario + self.scenario_inventory = [] + + # Inventory file path for persistence + self.inventory_path = self._get_inventory_path() + + # Setup scenario-specific file logging + self._setup_logging() + + logger.info(f"Initialized scenario: {self.scenario_name}") + + def _load_and_validate(self): + """Load the scenario YAML file and validate against the schema. + + Returns: + Parsed scenario data dictionary + + Raises: + ScenarioError: If the file cannot be loaded or validation fails + """ + if not self.scenario_path.exists(): + raise ScenarioError(f"Scenario file not found: {self.scenario_path}") + + try: + with self.scenario_path.open() as f: + data = yaml.load(f) + except Exception as e: + raise ScenarioError(f"Failed to parse scenario file: {e}") from e + + # Validate against schema if available + schema = get_schema() + if schema: + try: + jsonschema.validate(instance=data, schema=schema) + except jsonschema.ValidationError as e: + raise ScenarioError(f"Scenario validation failed: {e.message}") from e + + return data + + def _apply_cli_config_overrides(self): + """Apply CLI config overrides to the scenario config. + + CLI config values are specified as dotted paths like: + config.settings.AnsibleTower.workflow_timeout=500 + """ + for key, value in self.cli_config.items(): + # Remove 'config.' prefix if present + if key.startswith("config."): + key = key[7:] + + # Navigate the config dict and set the value + parts = key.split(".") + target = self.config + for part in parts[:-1]: + if part not in target: + target[part] = {} + target = target[part] + target[parts[-1]] = value + + def _get_inventory_path(self): + """Get the path for the scenario-specific inventory file. + + Returns: + Path object for the inventory file + """ + if inv_path := self.config.get("inventory_path"): + return Path(inv_path) + return BROKER_DIRECTORY / f"scenario_{self.scenario_name}_inventory.yaml" + + def _setup_logging(self): + """Setup scenario-specific file logging.""" + log_dir = BROKER_DIRECTORY / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + # Note: Actual file handler setup would be done here + # For now, we rely on the main broker logger + + def _load_scenario_inventory(self): + """Load existing scenario inventory from disk if it exists.""" + if self.inventory_path.exists(): + inv_data = helpers.load_file(self.inventory_path, warn=False) + if inv_data: + # Reconstruct host objects from the inventory data + for host_data in inv_data: + try: + host = Broker(broker_settings=self._settings).reconstruct_host(host_data) + if host: + self.scenario_inventory.append(host) + except Exception as e: + logger.warning(f"Failed to reconstruct host from inventory: {e}") + + def _save_scenario_inventory(self): + """Save the current scenario inventory to disk.""" + inv_data = [host.to_dict() for host in self.scenario_inventory] + self.inventory_path.parent.mkdir(parents=True, exist_ok=True) + self.inventory_path.touch() + yaml.dump(inv_data, self.inventory_path) + + def _build_context(self, current_step_name, previous_step_memory): + """Build the Jinja2 template context for a step. + + Args: + current_step_name: Name of the current step + previous_step_memory: StepMemory of the previous step (or None) + + Returns: + Dictionary context for template rendering + """ + # Create a dict-like wrapper for steps that allows both attribute and dict access + steps_dict = {} + for name, mem in self.steps_memory.items(): + steps_dict[name] = mem + + return { + "step": self.steps_memory.get(current_step_name), + "previous_step": previous_step_memory, + "steps": steps_dict, + "scenario_inventory": self.scenario_inventory, + **self.variables, + } + + def _execute_step(self, step_data, previous_step_memory): + """Execute a single step. + + Args: + step_data: Dictionary containing step configuration + previous_step_memory: StepMemory of the previous step + + Returns: + StepMemory for this step + """ + step_name = step_data["name"] + action = step_data["action"] + + # Initialize or reset step memory + if step_name not in self.steps_memory: + self.steps_memory[step_name] = StepMemory(step_name) + step_mem = self.steps_memory[step_name] + step_mem.status = "running" + + # Build context for template rendering + context = self._build_context(step_name, previous_step_memory) + + # Check 'when' condition + if "when" in step_data: + try: + should_run = evaluate_condition(step_data["when"], context) + if not should_run: + logger.info(f"Skipping step '{step_name}' due to condition") + step_mem.status = "skipped" + return step_mem + except Exception as e: + logger.warning(f"Error evaluating 'when' condition for '{step_name}': {e}") + step_mem.status = "skipped" + return step_mem + + logger.info(f"Executing step: {step_name} (action: {action})") + + try: + # Render arguments with template context + arguments = recursive_render(step_data.get("arguments", {}), context) + + # Resolve target hosts if 'with' is specified + target_hosts = None + if "with" in step_data: + hosts_ref = step_data["with"]["hosts"] + target_hosts = resolve_hosts_reference(hosts_ref, self.scenario_inventory, context) + + # Execute the action (loop or single) + if "loop" in step_data: + result = self._execute_loop(step_data, arguments, target_hosts, context) + else: + parallel = step_data.get("parallel", True) + result = self._dispatch_action(step_data, arguments, target_hosts, parallel) + + # Update step memory + step_mem.output = result + step_mem.status = "completed" + + # Handle capture + if "capture" in step_data: + self._capture_output(step_data["capture"], result, context) + + except Exception as e: + logger.error(f"Step '{step_name}' failed: {e}") + step_mem.output = str(e) + step_mem._error = e + step_mem.status = "failed" + + # Handle on_error steps + if "on_error" in step_data: + logger.info(f"Executing on_error handler for step '{step_name}'") + try: + self._execute_steps(step_data["on_error"]) + except Exception as handler_err: + logger.error(f"on_error handler also failed: {handler_err}") + raise handler_err from e + elif step_data.get("exit_on_error", True): + raise ScenarioError(f"Step '{step_name}' failed and exit_on_error is True") from e + else: + logger.warning(f"Step '{step_name}' failed but exit_on_error=False, continuing") + + return step_mem + + def _execute_steps(self, steps_list): + """Execute a list of steps sequentially. + + Args: + steps_list: List of step dictionaries to execute + """ + previous_step = None + + for step_data in steps_list: + step_mem = self._execute_step(step_data, previous_step) + previous_step = step_mem + + # Save inventory after each step in case of failure + self._save_scenario_inventory() + + def _execute_loop(self, step_data, base_arguments, target_hosts, context): + """Execute a step in a loop over an iterable. + + Args: + step_data: Step configuration + base_arguments: Base arguments to use for each iteration + target_hosts: Target hosts (if any) + context: Template context + + Returns: + Dictionary mapping iteration items to their results + """ + loop_config = step_data["loop"] + iterable_expr = loop_config["iterable"] + iter_var_name = loop_config["iter_var"] + on_error = loop_config.get("on_error") + + # Resolve the iterable + if "@inv" in iterable_expr: + # It's an inventory filter + resolved_iterable = helpers.eval_filter( + self.scenario_inventory, iterable_expr, filter_key="inv" + ) + else: + # Try to render as a template + resolved_iterable = recursive_render(iterable_expr, context) + + # Ensure it's a list + if not isinstance(resolved_iterable, (list, tuple)): + resolved_iterable = [resolved_iterable] + + loop_output = {} + + for item in resolved_iterable: + # Create a loop-specific context + loop_context = context.copy() + loop_context[iter_var_name] = item + + # Re-render arguments with the loop variable + iter_args = recursive_render(base_arguments, loop_context) + + try: + result = self._dispatch_action(step_data, iter_args, target_hosts, parallel=False) + loop_output[str(item)] = result + except Exception as e: + if on_error == "continue": + logger.warning(f"Loop iteration failed for {item}: {e}, continuing...") + loop_output[str(item)] = {"error": str(e)} + else: + raise + + return loop_output + + def _dispatch_action(self, step_data, arguments, hosts=None, parallel=True): + """Dispatch an action to the appropriate handler. + + Args: + step_data: Step configuration + arguments: Rendered arguments for the action + hosts: Target hosts (if any) + parallel: Whether to run in parallel for multi-host actions + + Returns: + Result of the action + """ + action = step_data["action"] + step_name = step_data["name"] + + if action == "checkout": + return self._action_checkout(step_name, arguments) + elif action == "checkin": + return self._action_checkin(arguments, hosts) + elif action == "inventory": + return self._action_inventory(arguments) + elif action == "ssh": + return self._action_ssh(arguments, hosts, parallel) + elif action == "scp": + return self._action_scp(arguments, hosts, parallel) + elif action == "sftp": + return self._action_sftp(arguments, hosts, parallel) + elif action == "execute": + return self._action_execute(step_name, arguments) + elif action == "exit": + return self._action_exit(arguments) + elif action == "run_scenarios": + return self._action_run_scenarios(arguments) + else: + raise ScenarioError(f"Unknown action: {action}") + + def _action_checkout(self, step_name, arguments): + """Handle checkout action.""" + broker_inst = Broker(broker_settings=self._settings, **arguments) + self.steps_memory[step_name]._broker_inst = broker_inst + + hosts = broker_inst.checkout() + if not isinstance(hosts, list): + hosts = [hosts] + + # Add to scenario inventory + self.scenario_inventory.extend(hosts) + self._save_scenario_inventory() + + return hosts + + def _action_checkin(self, arguments, hosts): + """Handle checkin action.""" + if not hosts: + hosts = arguments.get("hosts", self.scenario_inventory) + + if not isinstance(hosts, list): + hosts = [hosts] + + Broker(hosts=hosts, broker_settings=self._settings).checkin() + + # Remove from scenario inventory + for host in hosts: + if host in self.scenario_inventory: + self.scenario_inventory.remove(host) + self._save_scenario_inventory() + + return True + + def _action_inventory(self, arguments): + """Handle inventory action.""" + if sync_provider := arguments.get("sync"): + Broker.sync_inventory(provider=sync_provider, broker_settings=self._settings) + return helpers.load_inventory() + return helpers.load_inventory(filter=arguments.get("filter")) + + def _action_ssh(self, arguments, hosts, parallel): + """Handle ssh (execute command) action.""" + if not hosts: + raise ScenarioError("SSH action requires target hosts") + + command = arguments.get("command") + if not command: + raise ScenarioError("SSH action requires 'command' argument") + + timeout = arguments.get("timeout") + + def run_on_host(host): + return host.execute(command, timeout=timeout) + + if parallel and len(hosts) > 1: + results = [] + with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + futures = {executor.submit(run_on_host, h): h for h in hosts} + for future in as_completed(futures): + results.append(future.result()) + return results + else: + return [run_on_host(h) for h in hosts] + + def _action_scp(self, arguments, hosts, parallel): + """Handle scp action.""" + if not hosts: + raise ScenarioError("SCP action requires target hosts") + + source = arguments.get("source") + destination = arguments.get("destination") + if not source or not destination: + raise ScenarioError("SCP action requires 'source' and 'destination' arguments") + + def scp_to_host(host): + # Use sftp_write for uploading files + host.session.sftp_write(source, destination) + return helpers.Result(stdout=f"Copied {source} to {destination}", stderr="", status=0) + + if parallel and len(hosts) > 1: + results = [] + with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + futures = {executor.submit(scp_to_host, h): h for h in hosts} + for future in as_completed(futures): + results.append(future.result()) + return results + else: + return [scp_to_host(h) for h in hosts] + + def _action_sftp(self, arguments, hosts, parallel): + """Handle sftp action.""" + if not hosts: + raise ScenarioError("SFTP action requires target hosts") + + source = arguments.get("source") + destination = arguments.get("destination") + direction = arguments.get("direction", "upload") + + def sftp_on_host(host): + if direction == "upload": + host.session.sftp_write(source, destination) + return helpers.Result( + stdout=f"Uploaded {source} to {destination}", stderr="", status=0 + ) + else: + host.session.sftp_read(source, destination) + return helpers.Result( + stdout=f"Downloaded {source} to {destination}", stderr="", status=0 + ) + + if parallel and len(hosts) > 1: + results = [] + with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + futures = {executor.submit(sftp_on_host, h): h for h in hosts} + for future in as_completed(futures): + results.append(future.result()) + return results + else: + return [sftp_on_host(h) for h in hosts] + + def _action_execute(self, step_name, arguments): + """Handle execute action (provider action).""" + broker_inst = Broker(broker_settings=self._settings, **arguments) + self.steps_memory[step_name]._broker_inst = broker_inst + return broker_inst.execute() + + def _action_exit(self, arguments): + """Handle exit action.""" + return_code = int(arguments.get("return_code", 0)) + message = arguments.get("message", "Scenario exited explicitly") + + if return_code != 0: + raise ScenarioError(f"Exit Action: {message} (code: {return_code})") + + logger.info(f"Exit Action: {message}") + # Use a special exception to signal exit + raise SystemExit(return_code) + + def _action_run_scenarios(self, arguments): + """Handle run_scenarios action.""" + paths = arguments.get("paths", []) + results = [] + + for path in paths: + runner = ScenarioRunner( + scenario_path=path, + cli_vars=self.cli_vars, + cli_config=self.cli_config, + ) + runner.run() + results.append({"path": path, "success": True}) + + return results + + def _capture_output(self, capture_config, result, context): + """Capture step output into a variable. + + Args: + capture_config: Configuration for capture (as, transform) + result: The step result to capture + context: Template context + """ + var_name = capture_config["as"] + transform = capture_config.get("transform") + + value_to_store = result + + if transform: + # Create a temporary context with step.output set to result + temp_context = context.copy() + if temp_context.get("step"): + # Make a copy to avoid modifying the original + step_dict = temp_context["step"].to_dict() + step_dict["output"] = result + temp_context["step"] = step_dict + + value_to_store = render_template(transform, temp_context) + + self.variables[var_name] = value_to_store + logger.debug(f"Captured variable '{var_name}'") + + def run(self): + """Execute the scenario. + + This is the main entry point for running a scenario. + + Raises: + ScenarioError: If the scenario execution fails + """ + logger.info(f"Starting scenario: {self.scenario_name}") + + # Load any existing scenario inventory + self._load_scenario_inventory() + + try: + self._execute_steps(self.data.get("steps", [])) + logger.info(f"Scenario '{self.scenario_name}' completed successfully") + except SystemExit as e: + # Normal exit from exit action + logger.info(f"Scenario '{self.scenario_name}' exited with code {e.code}") + if e.code != 0: + raise ScenarioError(f"Scenario exited with non-zero code: {e.code}") + except ScenarioError: + raise + except Exception as e: + raise ScenarioError(f"Scenario failed: {e}") from e + + def get_info(self): + """Get summary information about the scenario. + + Returns: + Dictionary containing scenario metadata + """ + return { + "name": self.scenario_name, + "path": str(self.scenario_path), + "config": self.config, + "variables": self.data.get("variables", {}), + "steps": [ + {"name": s["name"], "action": s["action"]} for s in self.data.get("steps", []) + ], + } + + +def validate_scenario(scenario_path): + """Validate a scenario file against the schema. + + Args: + scenario_path: Path to the scenario file + + Returns: + Tuple of (is_valid, error_message or None) + """ + path = Path(scenario_path) + if not path.exists(): + return False, f"Scenario file not found: {scenario_path}" + + try: + with path.open() as f: + data = yaml.load(f) + except Exception as e: + return False, f"Failed to parse YAML: {e}" + + schema = get_schema() + if not schema: + return True, "Schema not found, skipping validation" + + try: + jsonschema.validate(instance=data, schema=schema) + return True, None + except jsonschema.ValidationError as e: + return False, f"Validation error: {e.message}" From d6fe9aa89835370b1939b429d1f238f2c079802e Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Fri, 12 Dec 2025 17:15:32 -0500 Subject: [PATCH 08/10] Enhance scenario engine capabilities and `merge_dicts` behavior Features: - Add `output` action for scenario steps to write content to stdout, stderr, or files, with automatic format detection based on file extension. - Introduce `provider_info` action to query configured providers for available resources (e.g., Ansible Tower workflows, OpenStack images) and retrieve structured data for programmatic use. - Significantly enhance scenario loop capabilities: - Allow the `iterable` argument to be a Jinja2 expression, variable, or inventory filter (`@inv`, `@scenario_inv`). - Support tuple unpacking for `iter_var` (e.g., `key, value` for iterating over dict items). - Implement `capture.key` to dynamically set keys for loop output within the step memory. - Evaluate `when` conditions on a per-iteration basis within loops. - Allow `on_error` in scenario steps to be either a list of recovery steps or the string `"continue"` to proceed despite failures. - Export the new `save_file` helper for general use. - Add the `scenarios` command to the interactive Broker shell. - Provider `provider_help` methods (e.g., AnsibleTower, Beaker, Container, Foreman, OpenStack) now return structured data in addition to printing it, enabling programmatic use within scenarios. Fixes: - Correct the `helpers.merge_dicts` behavior to ensure values from the second dictionary argument consistently take precedence over the first. - Update all `merge_dicts` call sites (e.g., Broker initialization, inventory updates) to correctly reflect the desired precedence, ensuring user-provided arguments or new data override defaults or existing data. - Ensure scenario configuration values passed via CLI are deep-merged into existing settings rather than overwriting top-level dictionaries. - Resolve a definition order issue for `broker_shell` commands in `commands.py` by moving shell definition. Refactoring: - Improve scenario host resolution to support filtering against both the main Broker inventory (`@inv`) and the scenario-specific inventory (`@scenario_inv`), including proper host reconstruction. - Refine `_action_ssh`, `_action_scp`, and `_action_sftp` to return a single `Result` object for single-host operations and a dictionary mapping hostnames to `Result` objects for multiple hosts, simplifying templating. - Simplify `list_scenarios` implementation for clarity. - Add `_clear_scenario_inventory` to ensure scenarios start with a fresh inventory, preventing carry-over from previous runs. Documentation: - Update `scenario_schema.json` to reflect new actions, enhanced loop features, and flexible `on_error` handling. - Clarify the `merge_dicts` docstring regarding value precedence. - Add a `.gitignore` file to the `scenarios` directory to exclude non-versioned files. --- broker/broker.py | 3 +- broker/commands.py | 140 ++++----- broker/helpers/__init__.py | 2 + broker/helpers/dict_utils.py | 4 +- broker/helpers/file_utils.py | 69 +++++ broker/helpers/inventory.py | 3 +- broker/providers/ansible_tower.py | 18 ++ broker/providers/beaker.py | 2 + broker/providers/container.py | 3 + broker/providers/foreman.py | 2 + broker/providers/openstack.py | 11 + broker/scenario_schema.json | 32 ++- broker/scenarios.py | 459 +++++++++++++++++++++++++----- broker/settings.py | 12 +- scenarios/.gitignore | 4 + 15 files changed, 611 insertions(+), 153 deletions(-) create mode 100644 scenarios/.gitignore diff --git a/broker/broker.py b/broker/broker.py index 18674abb..0b514fa6 100644 --- a/broker/broker.py +++ b/broker/broker.py @@ -75,7 +75,8 @@ def __init__(self, broker_settings=None, **kwargs): # if a nick was specified, pull in the resolved arguments if "nick" in kwargs: nick = kwargs.pop("nick") - kwargs = helpers.merge_dicts(kwargs, helpers.resolve_nick(nick, self._settings)) + # Merge nick resolution with kwargs; kwargs takes precedence over nick values + kwargs = helpers.merge_dicts(helpers.resolve_nick(nick, self._settings), kwargs) logger.debug(f"kwargs after nick resolution {kwargs=}") # Allow users to more simply pass a host class instead of a dict if "host_class" in kwargs: diff --git a/broker/commands.py b/broker/commands.py index f1767b2f..d871dbc6 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -42,7 +42,10 @@ click.rich_click.COMMAND_GROUPS = { "broker": [ {"name": "Core Actions", "commands": ["checkout", "checkin", "inventory"]}, - {"name": "Extras", "commands": ["execute", "extend", "providers", "config", "scenarios", "shell"]}, + { + "name": "Extras", + "commands": ["execute", "extend", "providers", "config", "scenarios", "shell"], + }, ] } @@ -568,72 +571,6 @@ def validate(chunk): logger.warning(f"Validation failed: {err}") -def _make_shell_help_func(cmd, shell_instance): - """Create a help function that invokes the command with --help. - - This works around a compatibility issue between click_shell and rich_click where - the shell's built-in help system uses a standard HelpFormatter that lacks - rich_click's config attribute. - """ - - def help_func(): - # Invoke the command with --help which properly uses rich_click formatting - with contextlib.suppress(SystemExit): - cmd.main(["--help"], standalone_mode=False, parent=shell_instance.ctx) - - help_func.__name__ = f"help_{cmd.name}" - return help_func - - -@shell( - prompt="broker > ", - intro="Welcome to Broker's interactive shell.\nType 'help' for commands, 'exit' or 'quit' to leave.", -) -def broker_shell(): - """Start an interactive Broker shell session.""" - pass - - -# Register commands to the shell -broker_shell.add_command(checkout) -broker_shell.add_command(checkin) -broker_shell.add_command(inventory) -broker_shell.add_command(execute) -broker_shell.add_command(providers) -broker_shell.add_command(config) - - -# Shell-only commands (not available as normal sub-commands) -@broker_shell.command(name="reload_config") -def reload_config_cmd(): - """Reload Broker's configuration from disk. - - This clears the cached settings, forcing them to be re-read - from the settings file on next access. - """ - settings.settings._settings = None - setup_logging( - console_level=settings.settings.logging.console_level, - file_level=settings.settings.logging.file_level, - log_path=settings.settings.logging.log_path, - structured=settings.settings.logging.structured, - ) - CONSOLE.print("Configuration reloaded.") - - -# Patch help functions on the shell instance to work around click_shell/rich_click incompatibility -for cmd_name, cmd in broker_shell.commands.items(): - setattr(broker_shell.shell, f"help_{cmd_name}", _make_shell_help_func(cmd, broker_shell.shell)) - - -@cli.command(name="shell") -def shell_cmd(): - """Start an interactive Broker shell session. - - This provides a REPL-like interface for running Broker commands - without needing to prefix each with 'broker'. - """ - broker_shell(standalone_mode=False, args=[]) # --- Scenarios CLI Group --- @@ -751,3 +688,72 @@ def scenarios_validate(scenario): logger.warning(error_msg) else: logger.error(f"Scenario '{scenario}' is invalid: {error_msg}") + + +def _make_shell_help_func(cmd, shell_instance): + """Create a help function that invokes the command with --help. + + This works around a compatibility issue between click_shell and rich_click where + the shell's built-in help system uses a standard HelpFormatter that lacks + rich_click's config attribute. + """ + + def help_func(): + # Invoke the command with --help which properly uses rich_click formatting + with contextlib.suppress(SystemExit): + cmd.main(["--help"], standalone_mode=False, parent=shell_instance.ctx) + + help_func.__name__ = f"help_{cmd.name}" + return help_func + + +@shell( + prompt="broker > ", + intro="Welcome to Broker's interactive shell.\nType 'help' for commands, 'exit' or 'quit' to leave.", +) +def broker_shell(): + """Start an interactive Broker shell session.""" + pass + + +# Register commands to the shell +broker_shell.add_command(checkout) +broker_shell.add_command(checkin) +broker_shell.add_command(inventory) +broker_shell.add_command(execute) +broker_shell.add_command(providers) +broker_shell.add_command(config) +broker_shell.add_command(scenarios) + + +# Shell-only commands (not available as normal sub-commands) +@broker_shell.command(name="reload_config") +def reload_config_cmd(): + """Reload Broker's configuration from disk. + + This clears the cached settings, forcing them to be re-read + from the settings file on next access. + """ + settings.settings._settings = None + setup_logging( + console_level=settings.settings.logging.console_level, + file_level=settings.settings.logging.file_level, + log_path=settings.settings.logging.log_path, + structured=settings.settings.logging.structured, + ) + CONSOLE.print("Configuration reloaded.") + + +# Patch help functions on the shell instance to work around click_shell/rich_click incompatibility +for cmd_name, cmd in broker_shell.commands.items(): + setattr(broker_shell.shell, f"help_{cmd_name}", _make_shell_help_func(cmd, broker_shell.shell)) + + +@cli.command(name="shell") +def shell_cmd(): + """Start an interactive Broker shell session. + + This provides a REPL-like interface for running Broker commands + without needing to prefix each with 'broker'. + """ + broker_shell(standalone_mode=False, args=[]) diff --git a/broker/helpers/__init__.py b/broker/helpers/__init__.py index 3acacdfb..3d7e4196 100644 --- a/broker/helpers/__init__.py +++ b/broker/helpers/__init__.py @@ -23,6 +23,7 @@ data_to_tempfile, load_file, resolve_file_args, + save_file, temporary_tar, yaml, yaml_format, @@ -92,6 +93,7 @@ "merge_dicts", "resolve_file_args", "resolve_nick", + "save_file", "set_emit_file", "simple_retry", "temporary_tar", diff --git a/broker/helpers/dict_utils.py b/broker/helpers/dict_utils.py index b4ab5ff7..115fa892 100644 --- a/broker/helpers/dict_utils.py +++ b/broker/helpers/dict_utils.py @@ -12,10 +12,12 @@ def clean_dict(in_dict): def merge_dicts(dict1, dict2): """Merge two nested dictionaries together. + Values from dict2 take precedence over dict1 for duplicate keys. + :return: merged dictionary """ if not isinstance(dict1, MutableMapping) or not isinstance(dict2, MutableMapping): - return dict1 + return dict2 dict1 = clean_dict(dict1) dict2 = clean_dict(dict2) merged = {} diff --git a/broker/helpers/file_utils.py b/broker/helpers/file_utils.py index c17ab653..ac476e1b 100644 --- a/broker/helpers/file_utils.py +++ b/broker/helpers/file_utils.py @@ -1,5 +1,6 @@ """File handling utilities.""" +import contextlib from contextlib import contextmanager from io import BytesIO import json @@ -32,6 +33,74 @@ def load_file(file, warn=True): return yaml.load(file) +def save_file(file, data, mode="overwrite"): + """Save data to a file, using appropriate format based on file extension. + + Args: + file: Path to the file (string or Path object) + data: The data to save. Can be dict, list, or string. + mode: Write mode - "overwrite" (default) or "append" + + Returns: + Path object to the saved file + + The format is determined by the file extension: + - .json: Save as JSON with indentation + - .yaml/.yml: Save as YAML + - Other: Save as plain text (string conversion if needed) + """ + file = Path(file) + file.parent.mkdir(parents=True, exist_ok=True) + + # Determine format based on extension + suffix = file.suffix.lower() + + if suffix == ".json": + # For JSON, try to parse string data as JSON first + if isinstance(data, str): + with contextlib.suppress(json.JSONDecodeError): + data = json.loads(data) + content = json.dumps(data, indent=2, default=str) + elif suffix in (".yaml", ".yml"): + # For YAML, try to parse string data as structured data first + if isinstance(data, str): + data = _try_parse_structured_string(data) + output = BytesIO() + yaml.dump(data, output) + content = output.getvalue().decode("utf-8") + # Plain text - convert to string if needed + elif isinstance(data, (dict, list)): + content = yaml_format(data) + else: + content = str(data) + + # Write the content + if mode == "append": + with file.open("a") as f: + f.write(content) + if not content.endswith("\n"): + f.write("\n") + else: + file.write_text(content) + if not content.endswith("\n"): + with file.open("a") as f: + f.write("\n") + + logger.debug(f"Saved data to file: {file.absolute()}") + return file + + +def _try_parse_structured_string(data): + """Try to parse a string as JSON or YAML, returning original if parsing fails.""" + # Try JSON first + with contextlib.suppress(json.JSONDecodeError): + return json.loads(data) + # Then try YAML + with contextlib.suppress(Exception): + return yaml.load(data) + return data + + def resolve_file_args(broker_args): """Check for files being passed in as values to arguments then attempt to resolve them. diff --git a/broker/helpers/inventory.py b/broker/helpers/inventory.py index d635fa64..775533cb 100644 --- a/broker/helpers/inventory.py +++ b/broker/helpers/inventory.py @@ -71,7 +71,8 @@ def update_inventory(add=None, remove=None): "name" ) == new_host.get("name"): # update missing data in the new_host with the old_host data - new_host.update(merge_dicts(new_host, host)) + # new_host values take precedence over old host data + new_host.update(merge_dicts(host, new_host)) inv_data.remove(host) if add: inv_data.extend(add) diff --git a/broker/providers/ansible_tower.py b/broker/providers/ansible_tower.py index ad11b986..955c40a7 100644 --- a/broker/providers/ansible_tower.py +++ b/broker/providers/ansible_tower.py @@ -967,6 +967,12 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=("Variable", "Default Value"), ) rich_console.print(extras_table) + return { + "name": workflow, + "description": wfjt.description, + "inventory": default_inv["name"], + "extra_vars": json.loads(wfjt.extra_vars), + } elif workflows: workflows = [ workflow.name @@ -986,6 +992,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=False, ) rich_console.print(workflow_table) + return workflows[:results_limit] elif inventory: if inv := self._v2.inventory.get(name=inventory, kind="").results: inv = inv.pop() @@ -997,6 +1004,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor title="Inventory Details", ) rich_console.print(inv_table) + return {"id": inv.id, "name": inv.name, "description": inv.description} elif inventories: inv = [inv.name for inv in self._v2.inventory.get(kind="", page_size=1000).results] if not inv: @@ -1012,6 +1020,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=False, ) rich_console.print(inv_table) + return inv[:results_limit] elif job_template: if jt := self._v2.job_templates.get(name=job_template).results: jt = jt.pop() @@ -1030,6 +1039,12 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=("Variable", "Default Value"), ) rich_console.print(extras_table) + return { + "name": job_template, + "description": jt.description, + "inventory": default_inv["name"], + "extra_vars": json.loads(jt.extra_vars), + } elif job_templates: job_templates = [ job_template.name @@ -1051,6 +1066,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=False, ) rich_console.print(job_template_table) + return job_templates[:results_limit] elif templates: templates = list( set( @@ -1073,6 +1089,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor headers=False, ) rich_console.print(template_table) + return templates[:results_limit] elif flavors: flavors = self.execute(workflow="list-flavors", artifacts="last")["data_out"][ "list_flavors" @@ -1087,6 +1104,7 @@ def provider_help( # noqa: PLR0911, PLR0912, PLR0915 - Possible TODO refactor flavors[:results_limit], title="Available Flavors", _id=False ) rich_console.print(flavor_table) + return flavors[:results_limit] def release(self, name, broker_args=None): """Release the host back to the tower instance via the release workflow.""" diff --git a/broker/providers/beaker.py b/broker/providers/beaker.py index 95b1905b..676dc56b 100644 --- a/broker/providers/beaker.py +++ b/broker/providers/beaker.py @@ -138,6 +138,7 @@ def provider_help(self, jobs=False, job=None, **kwargs): job_xml = self.runtime.job_clone(job, prettyxml=True, dryrun=True).stdout syntax = Syntax(job_xml, "xml", theme="monokai", line_numbers=True) rich_console.print(syntax) + return {"job_id": job, "xml": job_xml} elif jobs: result = self.runtime.job_list(**kwargs).stdout.splitlines() if res_filter := kwargs.get("results_filter"): @@ -153,6 +154,7 @@ def provider_help(self, jobs=False, job=None, **kwargs): headers=False, ) rich_console.print(job_table) + return result def release(self, host_name, job_id): """Release a hosts reserved from Beaker by cancelling the job.""" diff --git a/broker/providers/container.py b/broker/providers/container.py index d3779d5c..1022899c 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -268,6 +268,7 @@ def provider_help( ) rich_console.print("\n[bold]Image Configuration[/bold]") rich_console.print(syntax) + return {"name": image_name, **image_info, "config": config} elif container_hosts: images = [ img.tags[0] @@ -287,6 +288,7 @@ def provider_help( headers=False, ) rich_console.print(image_table) + return images elif container_apps: images = [img.tags[0] for img in self.runtime.images if img.tags] if res_filter := kwargs.get("results_filter"): @@ -302,6 +304,7 @@ def provider_help( headers=False, ) rich_console.print(image_table) + return images def get_inventory(self, name_prefix): """Get all containers that have a matching name prefix.""" diff --git a/broker/providers/foreman.py b/broker/providers/foreman.py index dbb6d731..fc73820e 100644 --- a/broker/providers/foreman.py +++ b/broker/providers/foreman.py @@ -185,6 +185,7 @@ def provider_help( headers=False, ) rich_console.print(hostgroup_table) + return hostgroup_names elif hostgroup: data = self.runtime.hostgroup(name=hostgroup) if not data: @@ -207,6 +208,7 @@ def provider_help( title=f"{hostgroup} Information", ) rich_console.print(hostgroup_table) + return {"name": hostgroup, **display_data} def _compile_host_info(self, host): return { diff --git a/broker/providers/openstack.py b/broker/providers/openstack.py index 4a1fb6e7..62691d4d 100644 --- a/broker/providers/openstack.py +++ b/broker/providers/openstack.py @@ -182,17 +182,26 @@ def provider_help(self, images=False, flavors=False, networks=False, templates=F """Display OpenStack provider information.""" if images: logger.info("Available images:") + image_list = [] for image in self.connection.image.images(): if image.status == "active": logger.info(f" - {image.name} ({image.id})") + image_list.append({"name": image.name, "id": image.id}) + return image_list elif flavors: logger.info("Available flavors:") + flavor_list = [] for flavor in self.connection.compute.flavors(): logger.info(f" - {flavor.name} ({flavor.id})") + flavor_list.append({"name": flavor.name, "id": flavor.id}) + return flavor_list elif networks: logger.info("Available networks:") + network_list = [] for network in self.connection.network.networks(): logger.info(f" - {network.name} ({network.id})") + network_list.append({"name": network.name, "id": network.id}) + return network_list elif templates: templates = self._settings.OPENSTACK.get("templates", {}) if templates: @@ -201,8 +210,10 @@ def provider_help(self, images=False, flavors=False, networks=False, templates=F logger.info(f" - {template_name}: {template_config}") else: logger.info("No templates configured") + return templates else: logger.info("OpenStack provider configured and connected") + return None def construct_host(self, provider_params, host_classes, **kwargs): """Construct a host object from the provider_params and kwargs.""" diff --git a/broker/scenario_schema.json b/broker/scenario_schema.json index 96752138..289f0226 100644 --- a/broker/scenario_schema.json +++ b/broker/scenario_schema.json @@ -61,7 +61,9 @@ "sftp", "execute", "run_scenarios", - "exit" + "exit", + "output", + "provider_info" ] }, "arguments": { @@ -90,11 +92,11 @@ "properties": { "iterable": { "type": "string", - "description": "An inventory filter or a variable that resolves to a list." + "description": "An inventory filter, variable name, or expression that resolves to an iterable. Supports dict methods like 'my_dict.items()' for key-value iteration." }, "iter_var": { "type": "string", - "description": "The name of the variable to hold the current item during each loop iteration." + "description": "The name(s) of the variable(s) to hold the current item. Supports tuple unpacking with comma-separated names (e.g., 'key, value' for dict items)." }, "on_error": { "type": "string", @@ -116,17 +118,31 @@ "transform": { "type": "string", "description": "A templating expression to transform the step.output before saving it." + }, + "key": { + "type": "string", + "description": "For loops only: A templating expression to derive the dictionary key for each iteration. Has access to loop variables and 'result'. If not specified, defaults to the iter_var value." } }, "required": ["as"], "additionalProperties": false }, "on_error": { - "type": "array", - "description": "A list of nested steps to execute if the current step fails.", - "items": { - "$ref": "#/definitions/step" - } + "oneOf": [ + { + "type": "string", + "description": "Simple error handling: 'continue' to proceed despite errors.", + "enum": ["continue"] + }, + { + "type": "array", + "description": "A list of nested steps to execute if the current step fails.", + "items": { + "$ref": "#/definitions/step" + } + } + ], + "description": "Error handling behavior. Can be 'continue' to proceed despite errors, or an array of recovery steps to execute on failure." }, "exit_on_error": { "type": "boolean", diff --git a/broker/scenarios.py b/broker/scenarios.py index 31a46b7c..79c484c8 100644 --- a/broker/scenarios.py +++ b/broker/scenarios.py @@ -9,9 +9,9 @@ runner.run() """ +from concurrent.futures import ThreadPoolExecutor, as_completed import json import logging -from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path import jinja2 @@ -21,6 +21,7 @@ from broker import helpers from broker.broker import Broker from broker.exceptions import ScenarioError +from broker.providers import PROVIDERS from broker.settings import BROKER_DIRECTORY, create_settings logger = logging.getLogger(__name__) @@ -29,6 +30,12 @@ yaml.default_flow_style = False yaml.sort_keys = False +# Mapping of user-friendly argument names to Broker's internal argument names +# These are arguments that have a different name when passed to Broker directly +ARGUMENT_NAME_MAP = { + "count": "_count", +} + # Directory where scenarios are stored by default SCENARIOS_DIR = BROKER_DIRECTORY / "scenarios" SCENARIOS_DIR.mkdir(parents=True, exist_ok=True) @@ -95,11 +102,7 @@ def list_scenarios(): if not SCENARIOS_DIR.exists(): return [] - scenarios = [] - for file in SCENARIOS_DIR.iterdir(): - if file.suffix in (".yaml", ".yml"): - scenarios.append(file.stem) - return sorted(scenarios) + return sorted(f.stem for f in SCENARIOS_DIR.iterdir() if f.suffix in (".yaml", ".yml")) def render_template(template_str, context): @@ -131,6 +134,29 @@ def render_template(template_str, context): raise ScenarioError(f"Undefined variable in template: {e}") from e +def evaluate_expression(expression, context): + """Evaluate a Jinja2 expression and return the actual Python object. + + Unlike render_template which always returns a string, this function + returns the actual Python object resulting from the expression evaluation. + This is useful for getting iterables, dicts, etc. + + Args: + expression: A Jinja2 expression (without {{ }} delimiters) + context: Dictionary of variables for expression evaluation + + Returns: + The Python object resulting from the expression evaluation + """ + try: + env = jinja2.Environment(undefined=jinja2.StrictUndefined) + compiled_expr = env.compile_expression(expression) + return compiled_expr(**context) + except jinja2.UndefinedError as e: + logger.warning(f"Expression evaluation warning: {e}") + raise ScenarioError(f"Undefined variable in expression: {e}") from e + + def evaluate_condition(expression, context): """Evaluate a Jinja2 expression returning a boolean. @@ -182,13 +208,17 @@ def recursive_render(data, context): return data -def resolve_hosts_reference(hosts_ref, scenario_inventory, context): +def resolve_hosts_reference(hosts_ref, scenario_inventory, context, broker_inst=None): # noqa: PLR0911 """Resolve a hosts reference to a list of host objects. Args: - hosts_ref: Either 'scenario_inventory' or an inventory filter expression + hosts_ref: Either 'scenario_inventory', 'inventory', or an inventory filter expression. + Filter expressions can use: + - @inv: Filter against Broker's main inventory + - @scenario_inv: Filter against the scenario's inventory scenario_inventory: List of hosts checked out by this scenario context: Template context for rendering + broker_inst: Optional Broker instance for reconstructing hosts from inventory data Returns: List of host objects @@ -196,10 +226,24 @@ def resolve_hosts_reference(hosts_ref, scenario_inventory, context): if hosts_ref == "scenario_inventory": return scenario_inventory.copy() - # Check if it's a filter expression (contains @inv) + # Check if it's the main Broker inventory + if hosts_ref == "inventory": + inv_data = helpers.load_inventory() + if broker_inst and inv_data: + return [broker_inst.reconstruct_host(h) for h in inv_data] + return inv_data + + # Check if it's a filter expression for the scenario inventory + if "@scenario_inv" in hosts_ref: + return helpers.eval_filter(scenario_inventory, hosts_ref, filter_key="scenario_inv") + + # Check if it's a filter expression for the main inventory if "@inv" in hosts_ref: - # Filter against the scenario inventory using Broker's filter - return helpers.eval_filter(scenario_inventory, hosts_ref, filter_key="inv") + inv_data = helpers.load_inventory() + filtered = helpers.eval_filter(inv_data, hosts_ref, filter_key="inv") + if broker_inst and filtered: + return [broker_inst.reconstruct_host(h) for h in filtered] + return filtered # Try to render as a template and see if it's a variable rendered = render_template(hosts_ref, context) @@ -325,11 +369,10 @@ def _apply_cli_config_overrides(self): """ for key, value in self.cli_config.items(): # Remove 'config.' prefix if present - if key.startswith("config."): - key = key[7:] + config_key = key[7:] if key.startswith("config.") else key # Navigate the config dict and set the value - parts = key.split(".") + parts = config_key.split(".") target = self.config for part in parts[:-1]: if part not in target: @@ -348,25 +391,42 @@ def _get_inventory_path(self): return BROKER_DIRECTORY / f"scenario_{self.scenario_name}_inventory.yaml" def _setup_logging(self): - """Setup scenario-specific file logging.""" + """Set up scenario-specific file logging.""" log_dir = BROKER_DIRECTORY / "logs" log_dir.mkdir(parents=True, exist_ok=True) # Note: Actual file handler setup would be done here # For now, we rely on the main broker logger + def _reconstruct_host_safe(self, host_data): + """Safely reconstruct a host from inventory data. + + Args: + host_data: Dictionary of host data from inventory + + Returns: + Host object or None if reconstruction fails + """ + try: + return Broker(broker_settings=self._settings).reconstruct_host(host_data) + except (KeyError, ValueError, TypeError) as e: + logger.warning(f"Failed to reconstruct host from inventory: {e}") + return None + def _load_scenario_inventory(self): """Load existing scenario inventory from disk if it exists.""" if self.inventory_path.exists(): inv_data = helpers.load_file(self.inventory_path, warn=False) if inv_data: # Reconstruct host objects from the inventory data - for host_data in inv_data: - try: - host = Broker(broker_settings=self._settings).reconstruct_host(host_data) - if host: - self.scenario_inventory.append(host) - except Exception as e: - logger.warning(f"Failed to reconstruct host from inventory: {e}") + hosts = [self._reconstruct_host_safe(h) for h in inv_data] + self.scenario_inventory.extend(h for h in hosts if h is not None) + + def _clear_scenario_inventory(self): + """Clear scenario inventory for a fresh run.""" + self.scenario_inventory.clear() + if self.inventory_path.exists(): + self.inventory_path.unlink() + logger.debug(f"Cleared existing scenario inventory: {self.inventory_path}") def _save_scenario_inventory(self): """Save the current scenario inventory to disk.""" @@ -386,9 +446,7 @@ def _build_context(self, current_step_name, previous_step_memory): Dictionary context for template rendering """ # Create a dict-like wrapper for steps that allows both attribute and dict access - steps_dict = {} - for name, mem in self.steps_memory.items(): - steps_dict[name] = mem + steps_dict = dict(self.steps_memory.items()) return { "step": self.steps_memory.get(current_step_name), @@ -398,7 +456,7 @@ def _build_context(self, current_step_name, previous_step_memory): **self.variables, } - def _execute_step(self, step_data, previous_step_memory): + def _execute_step(self, step_data, previous_step_memory): # noqa: PLR0912, PLR0915 """Execute a single step. Args: @@ -420,15 +478,16 @@ def _execute_step(self, step_data, previous_step_memory): # Build context for template rendering context = self._build_context(step_name, previous_step_memory) - # Check 'when' condition - if "when" in step_data: + # Check 'when' condition only if there's no loop + # (for loops, the condition is evaluated per-iteration inside _execute_loop) + if "when" in step_data and "loop" not in step_data: try: should_run = evaluate_condition(step_data["when"], context) if not should_run: logger.info(f"Skipping step '{step_name}' due to condition") step_mem.status = "skipped" return step_mem - except Exception as e: + except (jinja2.TemplateError, ScenarioError) as e: logger.warning(f"Error evaluating 'when' condition for '{step_name}': {e}") step_mem.status = "skipped" return step_mem @@ -436,19 +495,24 @@ def _execute_step(self, step_data, previous_step_memory): logger.info(f"Executing step: {step_name} (action: {action})") try: - # Render arguments with template context - arguments = recursive_render(step_data.get("arguments", {}), context) - # Resolve target hosts if 'with' is specified target_hosts = None if "with" in step_data: hosts_ref = step_data["with"]["hosts"] - target_hosts = resolve_hosts_reference(hosts_ref, self.scenario_inventory, context) + broker_inst = Broker(broker_settings=self._settings) + target_hosts = resolve_hosts_reference( + hosts_ref, self.scenario_inventory, context, broker_inst + ) # Execute the action (loop or single) if "loop" in step_data: - result = self._execute_loop(step_data, arguments, target_hosts, context) + # For loops, pass raw arguments - they'll be rendered per-iteration + # with loop variables available in context + raw_arguments = step_data.get("arguments", {}) + result = self._execute_loop(step_data, raw_arguments, target_hosts, context) else: + # Render arguments with template context + arguments = recursive_render(step_data.get("arguments", {}), context) parallel = step_data.get("parallel", True) result = self._dispatch_action(step_data, arguments, target_hosts, parallel) @@ -466,11 +530,14 @@ def _execute_step(self, step_data, previous_step_memory): step_mem._error = e step_mem.status = "failed" - # Handle on_error steps - if "on_error" in step_data: + # Handle on_error - can be "continue" string or list of recovery steps + on_error = step_data.get("on_error") + if on_error == "continue": + logger.warning(f"Step '{step_name}' failed but on_error=continue, continuing") + elif isinstance(on_error, list): logger.info(f"Executing on_error handler for step '{step_name}'") try: - self._execute_steps(step_data["on_error"]) + self._execute_steps(on_error) except Exception as handler_err: logger.error(f"on_error handler also failed: {handler_err}") raise handler_err from e @@ -496,7 +563,7 @@ def _execute_steps(self, steps_list): # Save inventory after each step in case of failure self._save_scenario_inventory() - def _execute_loop(self, step_data, base_arguments, target_hosts, context): + def _execute_loop(self, step_data, base_arguments, target_hosts, context): # noqa: PLR0912, PLR0915 """Execute a step in a loop over an iterable. Args: @@ -514,42 +581,96 @@ def _execute_loop(self, step_data, base_arguments, target_hosts, context): on_error = loop_config.get("on_error") # Resolve the iterable - if "@inv" in iterable_expr: - # It's an inventory filter + if "@scenario_inv" in iterable_expr: + # Filter against scenario inventory resolved_iterable = helpers.eval_filter( - self.scenario_inventory, iterable_expr, filter_key="inv" + self.scenario_inventory, iterable_expr, filter_key="scenario_inv" ) + elif "@inv" in iterable_expr: + # Filter against main broker inventory and reconstruct hosts + inv_data = helpers.load_inventory() + filtered = helpers.eval_filter(inv_data, iterable_expr, filter_key="inv") + broker_inst = Broker(broker_settings=self._settings) + resolved_iterable = [broker_inst.reconstruct_host(h) for h in filtered] else: - # Try to render as a template - resolved_iterable = recursive_render(iterable_expr, context) + # Evaluate as a Jinja2 expression to get the actual Python object + # Strip {{ }} if present, since evaluate_expression expects raw expression + expr = iterable_expr.strip() + if expr.startswith("{{") and expr.endswith("}}"): + expr = expr[2:-2].strip() + resolved_iterable = evaluate_expression(expr, context) + + # Ensure it's iterable - handle dicts specially + if isinstance(resolved_iterable, dict): + # Convert dict to list of tuples for iteration + resolved_iterable = list(resolved_iterable.items()) + elif not isinstance(resolved_iterable, (list, tuple)): + # Convert iterables like dict_items to a list + try: + resolved_iterable = list(resolved_iterable) + except TypeError: + resolved_iterable = [resolved_iterable] + + # Parse iter_var - support "key, value" syntax for tuple unpacking + iter_var_names = [v.strip() for v in iter_var_name.split(",")] - # Ensure it's a list - if not isinstance(resolved_iterable, (list, tuple)): - resolved_iterable = [resolved_iterable] + # Check for capture.key to determine how to key the loop output + capture_config = step_data.get("capture", {}) + capture_key_expr = capture_config.get("key") loop_output = {} for item in resolved_iterable: # Create a loop-specific context loop_context = context.copy() - loop_context[iter_var_name] = item + + # Handle tuple unpacking for multiple iter_var names + if len(iter_var_names) > 1 and isinstance(item, (list, tuple)): + for var_name, value in zip(iter_var_names, item): + loop_context[var_name] = value + default_loop_key = str(item[0]) if item else str(item) + else: + loop_context[iter_var_names[0]] = item + default_loop_key = str(item) + + # Check 'when' condition for this iteration (if present) + when_condition = step_data.get("when") + if when_condition: + try: + should_run = evaluate_condition(when_condition, loop_context) + if not should_run: + logger.debug(f"Skipping loop iteration for {item} due to condition") + continue + except (jinja2.TemplateError, ScenarioError) as e: + logger.warning(f"Error evaluating 'when' condition for iteration {item}: {e}") + continue # Re-render arguments with the loop variable iter_args = recursive_render(base_arguments, loop_context) try: result = self._dispatch_action(step_data, iter_args, target_hosts, parallel=False) - loop_output[str(item)] = result + + # Determine the key for this iteration's result + if capture_key_expr: + # Evaluate the key expression with loop context (including result) + key_context = loop_context.copy() + key_context["result"] = result + loop_key = str(render_template(f"{{{{ {capture_key_expr} }}}}", key_context)) + else: + loop_key = default_loop_key + + loop_output[loop_key] = result except Exception as e: if on_error == "continue": logger.warning(f"Loop iteration failed for {item}: {e}, continuing...") - loop_output[str(item)] = {"error": str(e)} + loop_output[default_loop_key] = {"error": str(e)} else: raise return loop_output - def _dispatch_action(self, step_data, arguments, hosts=None, parallel=True): + def _dispatch_action(self, step_data, arguments, hosts=None, parallel=True): # noqa: PLR0911 """Dispatch an action to the appropriate handler. Args: @@ -582,12 +703,32 @@ def _dispatch_action(self, step_data, arguments, hosts=None, parallel=True): return self._action_exit(arguments) elif action == "run_scenarios": return self._action_run_scenarios(arguments) + elif action == "output": + return self._action_output(arguments) + elif action == "provider_info": + return self._action_provider_info(step_name, arguments) else: raise ScenarioError(f"Unknown action: {action}") + def _map_argument_names(self, arguments): + """Map user-friendly argument names to Broker's internal names. + + Args: + arguments: Dictionary of arguments from the scenario + + Returns: + Dictionary with argument names mapped to Broker's expected names + """ + mapped = {} + for key, value in arguments.items(): + mapped_key = ARGUMENT_NAME_MAP.get(key, key) + mapped[mapped_key] = value + return mapped + def _action_checkout(self, step_name, arguments): """Handle checkout action.""" - broker_inst = Broker(broker_settings=self._settings, **arguments) + mapped_args = self._map_argument_names(arguments) + broker_inst = Broker(broker_settings=self._settings, **mapped_args) self.steps_memory[step_name]._broker_inst = broker_inst hosts = broker_inst.checkout() @@ -626,7 +767,12 @@ def _action_inventory(self, arguments): return helpers.load_inventory(filter=arguments.get("filter")) def _action_ssh(self, arguments, hosts, parallel): - """Handle ssh (execute command) action.""" + """Handle ssh (execute command) action. + + Returns: + A single Result object if there's only one host, + otherwise a dict mapping hostname to Result objects. + """ if not hosts: raise ScenarioError("SSH action requires target hosts") @@ -637,20 +783,31 @@ def _action_ssh(self, arguments, hosts, parallel): timeout = arguments.get("timeout") def run_on_host(host): - return host.execute(command, timeout=timeout) + return (host.hostname, host.execute(command, timeout=timeout)) + + if len(hosts) == 1: + # Return single result directly for easier template access + return hosts[0].execute(command, timeout=timeout) - if parallel and len(hosts) > 1: - results = [] + # Multiple hosts: return dict mapping hostname to result + if parallel: + results = {} with ThreadPoolExecutor(max_workers=len(hosts)) as executor: - futures = {executor.submit(run_on_host, h): h for h in hosts} + futures = [executor.submit(run_on_host, h) for h in hosts] for future in as_completed(futures): - results.append(future.result()) + hostname, result = future.result() + results[hostname] = result return results else: - return [run_on_host(h) for h in hosts] + return {h.hostname: h.execute(command, timeout=timeout) for h in hosts} def _action_scp(self, arguments, hosts, parallel): - """Handle scp action.""" + """Handle scp action. + + Returns: + A single Result object if there's only one host, + otherwise a dict mapping hostname to Result objects. + """ if not hosts: raise ScenarioError("SCP action requires target hosts") @@ -662,20 +819,35 @@ def _action_scp(self, arguments, hosts, parallel): def scp_to_host(host): # Use sftp_write for uploading files host.session.sftp_write(source, destination) + return ( + host.hostname, + helpers.Result(stdout=f"Copied {source} to {destination}", stderr="", status=0), + ) + + if len(hosts) == 1: + # Return single result directly for easier template access + hosts[0].session.sftp_write(source, destination) return helpers.Result(stdout=f"Copied {source} to {destination}", stderr="", status=0) - if parallel and len(hosts) > 1: - results = [] + # Multiple hosts: return dict mapping hostname to result + if parallel: + results = {} with ThreadPoolExecutor(max_workers=len(hosts)) as executor: - futures = {executor.submit(scp_to_host, h): h for h in hosts} + futures = [executor.submit(scp_to_host, h) for h in hosts] for future in as_completed(futures): - results.append(future.result()) + hostname, result = future.result() + results[hostname] = result return results else: - return [scp_to_host(h) for h in hosts] + return {h.hostname: scp_to_host(h)[1] for h in hosts} def _action_sftp(self, arguments, hosts, parallel): - """Handle sftp action.""" + """Handle sftp action. + + Returns: + A single Result object if there's only one host, + otherwise a dict mapping hostname to Result objects. + """ if not hosts: raise ScenarioError("SFTP action requires target hosts") @@ -684,6 +856,19 @@ def _action_sftp(self, arguments, hosts, parallel): direction = arguments.get("direction", "upload") def sftp_on_host(host): + if direction == "upload": + host.session.sftp_write(source, destination) + result = helpers.Result( + stdout=f"Uploaded {source} to {destination}", stderr="", status=0 + ) + else: + host.session.sftp_read(source, destination) + result = helpers.Result( + stdout=f"Downloaded {source} to {destination}", stderr="", status=0 + ) + return (host.hostname, result) + + def sftp_single(host): if direction == "upload": host.session.sftp_write(source, destination) return helpers.Result( @@ -695,15 +880,21 @@ def sftp_on_host(host): stdout=f"Downloaded {source} to {destination}", stderr="", status=0 ) - if parallel and len(hosts) > 1: - results = [] + if len(hosts) == 1: + # Return single result directly for easier template access + return sftp_single(hosts[0]) + + # Multiple hosts: return dict mapping hostname to result + if parallel: + results = {} with ThreadPoolExecutor(max_workers=len(hosts)) as executor: - futures = {executor.submit(sftp_on_host, h): h for h in hosts} + futures = [executor.submit(sftp_on_host, h) for h in hosts] for future in as_completed(futures): - results.append(future.result()) + hostname, result = future.result() + results[hostname] = result return results else: - return [sftp_on_host(h) for h in hosts] + return {h.hostname: sftp_on_host(h)[1] for h in hosts} def _action_execute(self, step_name, arguments): """Handle execute action (provider action).""" @@ -739,6 +930,128 @@ def _action_run_scenarios(self, arguments): return results + def _action_output(self, arguments): + """Handle output action - write content to stdout, stderr, or a file. + + Args: + arguments: Dictionary containing: + - content: The content to output (required). Can be a template string, + a variable name, or raw data. + - destination: Where to write the output. Options: + - "stdout" (default): Write to standard output + - "stderr": Write to standard error + - A file path: Write/append to the specified file + (.json and .yaml/.yml files will be formatted appropriately) + - mode: For file destinations only: + - "overwrite" (default): Overwrite existing file or create new + - "append": Append to existing file or create new + + Returns: + The content that was written + """ + import sys + + content = arguments.get("content") + if content is None: + raise ScenarioError("Output action requires 'content' argument") + + # Check if content is a variable name (string that matches a captured variable) + if isinstance(content, str) and content in self.variables: + content = self.variables[content] + + destination = arguments.get("destination", "stdout") + mode = arguments.get("mode", "overwrite") + + if destination == "stdout": + # Convert content to string for stdout + if not isinstance(content, str): + content = helpers.yaml_format(content) + sys.stdout.write(content) + if not content.endswith("\n"): + sys.stdout.write("\n") + sys.stdout.flush() + elif destination == "stderr": + # Convert content to string for stderr + if not isinstance(content, str): + content = helpers.yaml_format(content) + sys.stderr.write(content) + if not content.endswith("\n"): + sys.stderr.write("\n") + sys.stderr.flush() + else: + # Treat as file path - use save_file helper for format-aware saving + helpers.save_file(destination, content, mode=mode) + logger.debug(f"Wrote output to file: {destination}") + + return content + + def _action_provider_info(self, step_name, arguments): + """Handle provider_info action - query provider for available resources. + + Args: + step_name: Name of the current step + arguments: Dictionary containing: + - provider: The provider name (required), e.g., "AnsibleTower", "Container" + - query: What to query. Can be: + - A string for flag-style queries: "workflows", "inventories", etc. + - A dict for value-style queries: {"workflow": "my-workflow"} + - Additional provider-specific arguments (e.g., tower_inventory) + + Returns: + The data returned by the provider's provider_help method + + Example YAML: + - name: List workflows + action: provider_info + arguments: + provider: AnsibleTower + query: workflows + tower_inventory: my-inventory + + - name: Get workflow details + action: provider_info + arguments: + provider: AnsibleTower + query: + workflow: my-workflow-name + """ + provider_name = arguments.get("provider") + if not provider_name: + raise ScenarioError("provider_info action requires 'provider' argument") + + query = arguments.get("query") + if not query: + raise ScenarioError("provider_info action requires 'query' argument") + + # Get the provider class + provider_cls = PROVIDERS.get(provider_name) + if not provider_cls: + available = ", ".join(PROVIDERS.keys()) + raise ScenarioError(f"Unknown provider: {provider_name}. Available: {available}") + + # Build kwargs for provider_help + # Start with all arguments except 'provider' and 'query' + help_kwargs = {k: v for k, v in arguments.items() if k not in ("provider", "query")} + + # Process the query argument + if isinstance(query, str): + # Flag-style query: query: "workflows" -> workflows=True + help_kwargs[query] = True + elif isinstance(query, dict): + # Value-style query: query: {workflow: "name"} -> workflow="name" + help_kwargs.update(query) + else: + raise ScenarioError(f"Invalid query type: {type(query)}. Expected str or dict.") + + # Instantiate the provider and call provider_help + try: + provider_inst = provider_cls(broker_settings=self._settings) + self.steps_memory[step_name]._broker_inst = provider_inst + result = provider_inst.provider_help(**help_kwargs) + return result + except Exception as e: + raise ScenarioError(f"provider_info failed for {provider_name}: {e}") from e + def _capture_output(self, capture_config, result, context): """Capture step output into a variable. @@ -776,8 +1089,8 @@ def run(self): """ logger.info(f"Starting scenario: {self.scenario_name}") - # Load any existing scenario inventory - self._load_scenario_inventory() + # Clear any existing scenario inventory for a fresh run + self._clear_scenario_inventory() try: self._execute_steps(self.data.get("steps", [])) @@ -825,7 +1138,7 @@ def validate_scenario(scenario_path): try: with path.open() as f: data = yaml.load(f) - except Exception as e: + except (OSError, yaml.YAMLError) as e: return False, f"Failed to parse YAML: {e}" schema = get_schema() diff --git a/broker/settings.py b/broker/settings.py index a3022cb1..00f2633f 100644 --- a/broker/settings.py +++ b/broker/settings.py @@ -17,6 +17,7 @@ from broker.config_manager import ConfigManager from broker.exceptions import ConfigurationError +from broker.helpers import merge_dicts INTERACTIVE_MODE = ConfigManager.interactive_mode BROKER_DIRECTORY = Path.home().joinpath(".broker") @@ -101,10 +102,17 @@ def _create_and_configure_settings(file_path, file_exists, config_dict): # Remove vault loader if set somehow new_settings._loaders = [loader for loader in new_settings._loaders if "vault" not in loader] - # Add any configuration values passed in + # Add any configuration values passed in, merging nested dicts if config_dict: for key, value in config_dict.items(): - new_settings[key] = value + # Normalize key to uppercase for settings lookup + upper_key = key.upper() + existing = new_settings.get(upper_key) + if existing is not None and isinstance(existing, dict) and isinstance(value, dict): + # Deep merge the nested dictionaries + new_settings[upper_key] = merge_dicts(existing, value) + else: + new_settings[upper_key] = value return new_settings diff --git a/scenarios/.gitignore b/scenarios/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/scenarios/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore From b7693c1dc1cbfcb9f5f87aabd2683465de7c07c7 Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sun, 14 Dec 2025 21:08:22 -0500 Subject: [PATCH 09/10] Enhance scenario engine with advanced features and comprehensive documentation Features: - Implement custom scenario-specific logging, allowing users to configure a dedicated log file for each scenario with flexible path resolution rules. - Improve Jinja2 templating to preserve Python types (e.g., int, bool, list, dict) for simple variable references, preventing unnecessary string conversions. - Introduce a comprehensive JSON schema (`scenario_schema.json`) for validating scenario files, formalizing the structure of steps, actions, arguments, loops, conditionals, and error handling. This enables a richer set of scenario capabilities. Fixes: - Improve container hostname resolution in `container_info`, adding robust fallbacks to container name or short ID if the `Config` or `Hostname` attributes are missing. Refactoring: - Adjust the log level for scenario initialization messages from 'info' to 'debug' for cleaner default output. Documentation: - Add a new, in-depth `scenarios_tutorial.md` covering all aspects of scenario creation and execution, including available actions, configuration options, templating, loops, error handling, and examples. Tests: - Introduce new functional tests for scenarios, including a comprehensive test for the Container provider, and specific tests for deploying/checking in containers and querying provider details. - Add `test_scenarios.py` to integrate and run these new functional scenario tests. --- broker/logger.py | 172 --- broker/providers/container.py | 9 +- broker/scenario_schema.json | 4 + broker/scenarios.py | 61 +- scenarios/scenario_schema.json | 155 +++ scenarios/scenarios_tutorial.md | 1068 +++++++++++++++++ .../comprehensive_container_test.yaml | 415 +++++++ .../scenarios/deploy_checkin_containers.yaml | 19 + .../functional/scenarios/deploy_details.yaml | 29 + tests/functional/test_scenarios.py | 27 + 10 files changed, 1779 insertions(+), 180 deletions(-) delete mode 100644 broker/logger.py create mode 100644 scenarios/scenario_schema.json create mode 100644 scenarios/scenarios_tutorial.md create mode 100644 tests/functional/scenarios/comprehensive_container_test.yaml create mode 100644 tests/functional/scenarios/deploy_checkin_containers.yaml create mode 100644 tests/functional/scenarios/deploy_details.yaml create mode 100644 tests/functional/test_scenarios.py diff --git a/broker/logger.py b/broker/logger.py deleted file mode 100644 index 87d33b78..00000000 --- a/broker/logger.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Module handling internal and dependency logging.""" - -import copy -from enum import IntEnum -import logging - -import logzero - -from broker.settings import BROKER_DIRECTORY - - -class LOG_LEVEL(IntEnum): - """Bare class for log levels. Trace is added for custom logging.""" - - TRACE = 5 - DEBUG = logging.DEBUG - INFO = logging.INFO - WARNING = logging.WARNING - ERROR = logging.ERROR - - -DEFAULT_FILE_LEVEL = LOG_LEVEL.DEBUG # Default file logging level if not overridden - - -class RedactingFilter(logging.Filter): - """Custom logging.Filter to redact secrets from the Dynaconf config.""" - - def __init__(self, sensitive): - super().__init__() - self._sensitive = sensitive - - def filter(self, record): - """Filter the record and redact the sensitive keys.""" - if isinstance(record.args, dict): - record.args = self.redact_dynaconf(record.args) - else: - record.args = tuple(self.redact_dynaconf(arg) for arg in record.args) - return True - - def redact_dynaconf(self, data): - """Go over the data and redact all values of keys that match the sensitive ones.""" - if isinstance(data, list | tuple): - data_copy = [self.redact_dynaconf(item) for item in data] - elif isinstance(data, dict): - data_copy = copy.deepcopy(data) - for k, v in data_copy.items(): - if isinstance(v, dict | list): - data_copy[k] = self.redact_dynaconf(v) - elif k in self._sensitive and v: - data_copy[k] = "******" - else: - data_copy = data - return data_copy - - -_sensitive = ["password", "pword", "token", "host_password"] -logging.addLevelName("TRACE", LOG_LEVEL.TRACE) -logzero.DEFAULT_COLORS[LOG_LEVEL.TRACE.value] = logzero.colors.Fore.MAGENTA - - -def try_disable_urllib3_warnings(): - """Attempt to disable urllib3 InsecureRequestWarning if urllib3 is available.""" - try: - import urllib3 - except ImportError: - logzero.logger.debug("urllib3 not installed, skipping urllib3 warning patch.") - return - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - -def try_patch_awx_for_verbosity(): - """Patch the awxkit API to enable trace-level logging of API calls to Ansible provider.""" - try: - from awxkit import api - except ImportError: - logzero.logger.debug("awxkit not installed, skipping awxkit logging patch.") - return - awx_log = api.client.log - awx_log.parent = logzero.logger - - def patch(cls, name): - func = getattr(cls, name) - - def the_patch(self, *args, **kwargs): - awx_log.log(LOG_LEVEL.TRACE.value, f"Calling {self=} {func=}(*{args=}, **{kwargs=}") - retval = func(self, *args, **kwargs) - awx_log.log( - LOG_LEVEL.TRACE.value, - f"Finished {self=} {func=}(*{args=}, **{kwargs=}) {retval=}", - ) - return retval - - setattr(cls, name, the_patch) - - for method in "delete get head options patch post put".split(): - patch(api.Connection, method) - - -def resolve_log_level(level): - """Resolve the log level from a string.""" - try: - log_level = LOG_LEVEL[level.upper()] - except KeyError: - log_level = LOG_LEVEL.INFO - return log_level - - -def formatter_factory(log_level, color=True): - """Create a logzero formatter based on the log level.""" - log_fmt = "%(color)s[%(levelname)s %(asctime)s]%(end_color)s %(message)s" - debug_fmt = ( - "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" - ) - formatter = logzero.LogFormatter( - fmt=debug_fmt if log_level <= LOG_LEVEL.DEBUG else log_fmt, color=color - ) - return formatter - - -def set_log_level(level): - """Set the log level for logzero.""" - log_level = LOG_LEVEL.INFO if level == "silent" else resolve_log_level(level) - logzero.formatter(formatter=formatter_factory(log_level)) - logzero.loglevel(level=log_level) - - -def set_file_logging(level, path="logs/broker.log"): - """Set the file logging for logzero.""" - silent = False - if level == "silent": - silent = True - log_level = LOG_LEVEL.INFO - else: - # Allow override of file logging level with if the new level is lower than the default. - # Otherwise, use the default value. - new_log_level = resolve_log_level(level) - log_level = new_log_level if new_log_level < DEFAULT_FILE_LEVEL else DEFAULT_FILE_LEVEL - - path = BROKER_DIRECTORY.joinpath(path) - path.parent.mkdir(parents=True, exist_ok=True) - logzero.logfile( - path, - loglevel=log_level.value, - maxBytes=1e9, - backupCount=3, - formatter=formatter_factory(log_level, color=False), - disableStderrLogger=silent, - ) - - -def setup_logzero( - level="info", - formatter=None, - file_level="debug", - name=None, - path="logs/broker.log", -): - """Call logzero setup with the given settings.""" - level = level or "info" - file_level = file_level or "debug" - path = path or "logs/broker.log" - set_log_level(level) - set_file_logging(file_level, path) - if formatter: - logzero.formatter(formatter) - logzero.logger.name = name or "broker" - logzero.logger.addFilter(RedactingFilter(_sensitive)) - - -try_disable_urllib3_warnings() -try_patch_awx_for_verbosity() -setup_logzero() diff --git a/broker/providers/container.py b/broker/providers/container.py index 1022899c..d57b7ed5 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -22,11 +22,18 @@ def container_info(container_inst): """Return a dict of container information.""" attr_dict = {"container_host": "Image", "_broker_origin": "Labels/broker.origin"} + # Config may not be present in list output, only in inspect output + # Fall back to container name or Id if hostname is not available + config = container_inst.attrs.get("Config", {}) + hostname = config.get("Hostname") if config else None + if not hostname: + # Use the container name or short id as fallback + hostname = container_inst.name or container_inst.attrs.get("Id", "")[:12] info = { "_broker_provider": "Container", "_broker_args": helpers.dict_from_paths(container_inst.attrs, attr_dict), "name": container_inst.name, - "hostname": container_inst.attrs["Config"].get("Hostname"), + "hostname": hostname, "image": container_inst.image.tags, "ports": container_inst.ports, } diff --git a/broker/scenario_schema.json b/broker/scenario_schema.json index 289f0226..2c484737 100644 --- a/broker/scenario_schema.json +++ b/broker/scenario_schema.json @@ -12,6 +12,10 @@ "type": "string", "description": "The file path to the scenario's dedicated inventory file." }, + "log_path": { + "type": "string", + "description": "Custom log file path. If a filename only, logs to BROKER_DIRECTORY/logs/. If an absolute path with filename, uses as-is. If an absolute directory path, uses {path}/{scenario_name}.log. If not specified, uses the main broker.log file." + }, "settings": { "type": "object", "description": "A nested map of provider-specific settings.", diff --git a/broker/scenarios.py b/broker/scenarios.py index 79c484c8..ef8028b1 100644 --- a/broker/scenarios.py +++ b/broker/scenarios.py @@ -21,6 +21,7 @@ from broker import helpers from broker.broker import Broker from broker.exceptions import ScenarioError +from broker.logging import setup_logging from broker.providers import PROVIDERS from broker.settings import BROKER_DIRECTORY, create_settings @@ -111,12 +112,16 @@ def render_template(template_str, context): If the input is not a string or doesn't contain template syntax, it is returned as-is. + For simple variable references like "{{ variable }}", this function + preserves the original Python type (int, bool, list, dict, etc.). + For complex templates with text or multiple expressions, it returns a string. + Args: template_str: String that may contain Jinja2 template syntax context: Dictionary of variables for template rendering Returns: - Rendered string or original value if not a template + Rendered value (preserving type for simple refs) or string for complex templates """ if not isinstance(template_str, str): return template_str @@ -125,6 +130,23 @@ def render_template(template_str, context): if "{{" not in template_str and "{%" not in template_str: return template_str + # Check if this is a simple variable reference like "{{ var }}" or "{{ obj.attr }}" + # If so, use evaluate_expression to preserve the Python type + stripped = template_str.strip() + if ( + stripped.startswith("{{") + and stripped.endswith("}}") + and stripped.count("{{") == 1 + and "{%" not in stripped + ): + # Extract the expression inside {{ }} + expr = stripped[2:-2].strip() + try: + return evaluate_expression(expr, context) + except ScenarioError: + # Fall through to standard rendering if expression evaluation fails + pass + try: env = jinja2.Environment(undefined=jinja2.StrictUndefined) template = env.from_string(template_str) @@ -331,7 +353,7 @@ def __init__(self, scenario_path, cli_vars=None, cli_config=None): # Setup scenario-specific file logging self._setup_logging() - logger.info(f"Initialized scenario: {self.scenario_name}") + logger.debug(f"Initialized scenario: {self.scenario_name}") def _load_and_validate(self): """Load the scenario YAML file and validate against the schema. @@ -391,11 +413,36 @@ def _get_inventory_path(self): return BROKER_DIRECTORY / f"scenario_{self.scenario_name}_inventory.yaml" def _setup_logging(self): - """Set up scenario-specific file logging.""" - log_dir = BROKER_DIRECTORY / "logs" - log_dir.mkdir(parents=True, exist_ok=True) - # Note: Actual file handler setup would be done here - # For now, we rely on the main broker logger + """Set up scenario-specific file logging. + + Path resolution rules for log_path config: + - If not specified: Use the default broker.log file (no reconfiguration) + - If filename only (no /): Use BROKER_DIRECTORY/logs/{filename} + - If absolute path with filename: Use as-is + - If absolute directory path: Use {path}/{scenario_name}.log + """ + log_path_config = self.config.get("log_path") + if not log_path_config: + # Use default broker.log - no reconfiguration needed + return + + log_path = Path(log_path_config) + + if not log_path.is_absolute(): + # Filename only - place in BROKER_DIRECTORY/logs/ + resolved_path = BROKER_DIRECTORY / "logs" / log_path + elif log_path.suffix: + # Absolute path with filename extension - use as-is + resolved_path = log_path + else: + # Absolute directory path - use {path}/{scenario_name}.log + resolved_path = log_path / f"{self.scenario_name}.log" + + # Ensure the parent directory exists + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # Reconfigure logging to use the custom log file + setup_logging(log_path=resolved_path) def _reconstruct_host_safe(self, host_data): """Safely reconstruct a host from inventory data. diff --git a/scenarios/scenario_schema.json b/scenarios/scenario_schema.json new file mode 100644 index 00000000..5f1e14dc --- /dev/null +++ b/scenarios/scenario_schema.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Scenario Schema", + "description": "A schema for validating scenario YAML files based on the defined specification.", + "type": "object", + "properties": { + "config": { + "type": "object", + "description": "Global configuration settings for the scenario execution.", + "properties": { + "inventory_path": { + "type": "string", + "description": "The file path to the scenario's dedicated inventory file." + }, + "settings": { + "type": "object", + "description": "A nested map of provider-specific settings.", + "additionalProperties": { + "type": "object" + } + } + }, + "additionalProperties": false + }, + "variables": { + "type": "object", + "description": "A key-value map of variables to be used within the steps.", + "additionalProperties": { + "type": ["string", "number", "boolean", "array", "object"] + } + }, + "steps": { + "type": "array", + "description": "A list of step objects that define the scenario's actions.", + "items": { + "$ref": "#/definitions/step" + } + } + }, + "required": [ + "steps" + ], + "additionalProperties": false, + "definitions": { + "step": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A human-readable name for the step." + }, + "action": { + "type": "string", + "description": "The type of action to perform.", + "enum": [ + "checkout", + "checkin", + "inventory", + "ssh", + "scp", + "sftp", + "execute", + "run_scenarios", + "exit", + "output", + "provider_info" + ] + }, + "arguments": { + "type": "object", + "description": "An optional key-value map of arguments specific to the chosen action." + }, + "with": { + "type": "object", + "description": "Specifies the target hosts for the action.", + "properties": { + "hosts": { + "type": "string", + "description": "Can be 'scenario_inventory' or an inventory filter expression." + } + }, + "required": ["hosts"], + "additionalProperties": false + }, + "when": { + "type": "string", + "description": "A conditional expression that must evaluate to true for the step to run." + }, + "loop": { + "type": "object", + "description": "Defines a loop to run the action multiple times.", + "properties": { + "iterable": { + "type": "string", + "description": "An inventory filter, variable name, or expression that resolves to an iterable. Supports dict methods like 'my_dict.items()' for key-value iteration." + }, + "iter_var": { + "type": "string", + "description": "The name(s) of the variable(s) to hold the current item. Supports tuple unpacking with comma-separated names (e.g., 'key, value' for dict items)." + }, + "on_error": { + "type": "string", + "description": "Defines behavior on loop item failure.", + "enum": ["continue"] + } + }, + "required": ["iterable", "iter_var"], + "additionalProperties": false + }, + "capture": { + "type": "object", + "description": "Captures the output of the step into a variable.", + "properties": { + "as": { + "type": "string", + "description": "The name of the new variable to store the result in." + }, + "transform": { + "type": "string", + "description": "A templating expression to transform the step.output before saving it." + }, + "key": { + "type": "string", + "description": "For loops only: A templating expression to derive the dictionary key for each iteration. Has access to loop variables and 'result'. If not specified, defaults to the iter_var value." + } + }, + "required": ["as"], + "additionalProperties": false + }, + "on_error": { + "type": "array", + "description": "A list of nested steps to execute if the current step fails.", + "items": { + "$ref": "#/definitions/step" + } + }, + "exit_on_error": { + "type": "boolean", + "description": "If False, the scenario will continue even if the step fails and no on_error block is defined.", + "default": true + }, + "parallel": { + "type": "boolean", + "description": "If False, forces the action to run sequentially when targeting multiple hosts.", + "default": true + } + }, + "required": [ + "name", + "action" + ], + "additionalProperties": false + } + } +} diff --git a/scenarios/scenarios_tutorial.md b/scenarios/scenarios_tutorial.md new file mode 100644 index 00000000..cabb4dd4 --- /dev/null +++ b/scenarios/scenarios_tutorial.md @@ -0,0 +1,1068 @@ +# Broker Scenarios Tutorial + +Scenarios are a powerful feature that allows you to chain multiple Broker actions together in YAML files. Instead of running separate `broker checkout`, `broker checkin`, and shell commands, you can define a complete workflow in a single scenario file. + +This tutorial covers everything you need to know to write effective scenarios. + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Scenario Structure](#scenario-structure) +3. [Available Actions](#available-actions) +4. [Step Configuration Options](#step-configuration-options) +5. [Variables and Templating](#variables-and-templating) +6. [Host Selection and Inventory Filters](#host-selection-and-inventory-filters) +7. [Loops](#loops) +8. [Error Handling](#error-handling) +9. [Capturing Output](#capturing-output) +10. [Complete Examples](#complete-examples) +11. [CLI Reference](#cli-reference) + +--- + +## Quick Start + +Here's a minimal scenario that checks out a container, runs a command, and checks it back in: + +```yaml +# my_first_scenario.yaml +steps: + - name: Provision a container + action: checkout + arguments: + container_host: ubi8 + + - name: Run a command + action: ssh + arguments: + command: "cat /etc/os-release" + with: + hosts: scenario_inventory + + - name: Release the container + action: checkin + with: + hosts: scenario_inventory +``` + +Save this file to `~/.broker/scenarios/my_first_scenario.yaml`, then run: + +```bash +broker scenarios execute my_first_scenario +``` + +--- + +## Scenario Structure + +A scenario file has three main sections: + +```yaml +# config: Optional global settings +config: + inventory_path: /path/to/custom_inventory.yaml # Custom inventory file + log_path: my_scenario.log # Custom log file + settings: # Provider-specific settings + Container: + runtime: podman + +# variables: Optional key-value pairs for use in steps +variables: + MY_IMAGE: ubi8 + INSTALL_PACKAGES: true + TIMEOUT: 60 + +# steps: Required list of actions to execute +steps: + - name: Step 1 + action: checkout + arguments: + container_host: "{{ MY_IMAGE }}" +``` + +### Config Section + +The `config` section allows you to customize scenario behavior: + +| Key | Description | +|-----|-------------| +| `inventory_path` | Path to a custom inventory file for this scenario | +| `log_path` | Custom log file path (see path resolution rules below) | +| `settings` | Nested map of provider-specific settings that override `broker_settings.yaml` | + +**Log Path Resolution Rules:** +- **Not specified**: Uses the default `broker.log` file in `{BROKER_DIRECTORY}/logs/` +- **Filename only** (e.g., `my_scenario.log`): Creates file in `{BROKER_DIRECTORY}/logs/` +- **Absolute path with filename** (e.g., `/var/log/broker/custom.log`): Uses as-is +- **Absolute directory** (e.g., `/var/log/broker/`): Creates `{scenario_name}.log` in that directory + +**Example: Override AnsibleTower timeout** +```yaml +config: + settings: + AnsibleTower: + workflow_timeout: 600 +``` + +### Variables Section + +Variables defined here are available throughout your scenario via Jinja2 templating: + +```yaml +variables: + RHEL_VERSION: "9.4" + HOST_COUNT: 3 + DEPLOY_CONFIG: + memory: 4096 + cpus: 2 + +steps: + - name: Deploy hosts + action: checkout + arguments: + workflow: deploy-rhel + rhel_version: "{{ RHEL_VERSION }}" + count: "{{ HOST_COUNT }}" +``` + +Variables can be overridden via CLI: +```bash +broker scenarios execute my_scenario --RHEL_VERSION 8.10 --HOST_COUNT 5 +``` + +--- + +## Available Actions + +Scenarios support 11 different actions: + +### `checkout` - Provision Hosts + +Checks out hosts from a provider (VMs, containers, etc.). + +```yaml +- name: Provision RHEL VM + action: checkout + arguments: + workflow: deploy-rhel # AnsibleTower workflow + rhel_version: "9.4" + count: 2 # Number of hosts + note: "Testing new feature" + +- name: Provision container + action: checkout + arguments: + container_host: ubi8 # Container image + ports: "22:2222 80:8080" # Port mappings + environment: "DEBUG=1" # Environment variables +``` + +**Output:** List of host objects, automatically added to `scenario_inventory`. + +### `checkin` - Release Hosts + +Releases hosts back to the provider. + +```yaml +- name: Release all scenario hosts + action: checkin + with: + hosts: scenario_inventory + +- name: Release specific hosts + action: checkin + with: + hosts: "@scenario_inv[0:2]" # First two hosts only +``` + +**Output:** `true` on success. + +### `ssh` - Execute Remote Commands + +Runs shell commands on target hosts. + +```yaml +- name: Check disk space + action: ssh + arguments: + command: "df -h" + timeout: 30 # Optional timeout in seconds + with: + hosts: scenario_inventory +``` + +**Output:** +- Single host: A Result object with `stdout`, `stderr`, and `status` attributes +- Multiple hosts: Dictionary mapping hostname to Result object + +### `scp` - Copy Files to Hosts + +Uploads files to remote hosts. + +```yaml +- name: Upload config file + action: scp + arguments: + source: /local/path/config.yaml + destination: /etc/myapp/config.yaml + with: + hosts: scenario_inventory +``` + +**Output:** Result object with success message. + +### `sftp` - Transfer Files + +Transfers files via SFTP (supports both upload and download). + +```yaml +- name: Upload script + action: sftp + arguments: + source: ./scripts/setup.sh + destination: /root/setup.sh + direction: upload # Default + +- name: Download logs + action: sftp + arguments: + source: /var/log/app.log + destination: ./logs/app.log + direction: download + with: + hosts: scenario_inventory +``` + +**Output:** Result object with success message. + +### `execute` - Run Provider Actions + +Executes arbitrary provider actions (like power operations, extend lease, etc.). + +```yaml +- name: Extend VM lease + action: execute + arguments: + workflow: extend-lease + source_vm: "{{ host.name }}" + extend_days: 7 +``` + +**Output:** Provider-specific result. + +### `provider_info` - Query Provider Resources + +Queries a provider for available resources (workflows, images, inventories, etc.). + +```yaml +# Flag-style query: list all workflows +- name: List available workflows + action: provider_info + arguments: + provider: AnsibleTower + query: workflows + tower_inventory: my-inventory + +# Value-style query: get specific workflow details +- name: Get workflow details + action: provider_info + arguments: + provider: AnsibleTower + query: + workflow: deploy-rhel +``` + +**Available queries by provider:** + +| Provider | Flag Queries | Value Queries | +|----------|--------------|---------------| +| AnsibleTower | `workflows`, `inventories`, `job_templates`, `templates`, `flavors` | `workflow`, `inventory`, `job_template` | +| Container | `container_hosts`, `container_apps` | `container_host`, `container_app` | +| Beaker | `jobs` | `job` | +| Foreman | `hostgroups` | `hostgroup` | +| OpenStack | `images`, `flavors`, `networks`, `templates` | - | + +**Output:** Dictionary or list of provider resource data. + +### `inventory` - Query or Sync Inventory + +Works with Broker's inventory system. + +```yaml +# Sync inventory from a provider +- name: Sync Tower inventory + action: inventory + arguments: + sync: AnsibleTower + +# Filter inventory +- name: Get RHEL hosts + action: inventory + arguments: + filter: "'rhel' in @inv.name" +``` + +**Output:** Inventory data (list of host dictionaries). + +### `output` - Write Content + +Writes content to stdout, stderr, or a file. + +```yaml +# Write to stdout +- name: Display message + action: output + arguments: + content: "Deployment complete!" + destination: stdout # Default + +# Write to file (format auto-detected by extension) +- name: Save results to JSON + action: output + arguments: + content: "{{ workflow_results }}" + destination: /tmp/results.json + mode: overwrite # or "append" + +# Write to YAML file +- name: Save hosts list + action: output + arguments: + content: "{{ scenario_inventory }}" + destination: ./hosts.yaml +``` + +**Output:** The content that was written. + +### `exit` - Exit Scenario + +Terminates scenario execution with a return code. + +```yaml +- name: Exit on failure condition + action: exit + arguments: + return_code: 1 + message: "Required condition not met" + when: not deployment_successful + +- name: Exit successfully + action: exit + arguments: + return_code: 0 + message: "All tests passed" +``` + +### `run_scenarios` - Execute Other Scenarios + +Chains other scenario files for modular workflows. + +```yaml +- name: Run cleanup scenarios + action: run_scenarios + arguments: + paths: + - /path/to/cleanup_vms.yaml + - /path/to/cleanup_containers.yaml +``` + +**Output:** List of `{path, success}` dictionaries. + +--- + +## Step Configuration Options + +Each step supports several configuration options: + +### `name` (required) + +Human-readable identifier for the step. Used for logging and step memory references. + +```yaml +- name: Deploy production servers +``` + +### `action` (required) + +One of the 11 available actions. + +### `arguments` (optional) + +Key-value pairs passed to the action. Supports templating. + +### `with` - Target Host Selection + +Specifies which hosts an action should target. + +```yaml +with: + hosts: scenario_inventory # All hosts from this scenario + hosts: inventory # All hosts from main Broker inventory + hosts: "@scenario_inv[0]" # First scenario host + hosts: "'rhel' in @inv.name" # Filtered hosts +``` + +### `when` - Conditional Execution + +Execute step only if condition is true. + +```yaml +- name: Install packages + action: ssh + arguments: + command: "dnf install -y httpd" + with: + hosts: scenario_inventory + when: INSTALL_PACKAGES == true + +- name: Cleanup only if failed + action: checkin + with: + hosts: scenario_inventory + when: previous_step.status == 'failed' +``` + +### `parallel` - Control Parallel Execution + +For multi-host actions, control whether they run in parallel or sequentially. + +```yaml +- name: Run migrations sequentially + action: ssh + arguments: + command: "./migrate.sh" + with: + hosts: scenario_inventory + parallel: false # Run one at a time (default: true) +``` + +### `exit_on_error` - Continue on Failure + +By default, scenarios stop on step failure. Set to `false` to continue. + +```yaml +- name: Optional cleanup + action: ssh + arguments: + command: "rm -rf /tmp/cache" + with: + hosts: scenario_inventory + exit_on_error: false # Continue even if this fails +``` + +--- + +## Variables and Templating + +Scenarios use Jinja2 templating for dynamic values. + +### Template Syntax + +```yaml +# Simple variable +"{{ variable_name }}" + +# Attribute access +"{{ host.hostname }}" + +# Method calls +"{{ result.stdout.strip() }}" + +# String interpolation +"Host {{ hostname }} returned: {{ result.stdout }}" + +# Expressions +"{{ count * 2 }}" +``` + +### Available Context Variables + +| Variable | Description | +|----------|-------------| +| `step` | Current step's memory (name, output, status) | +| `previous_step` | Previous step's memory | +| `steps` | Dictionary of all steps by name | +| `scenario_inventory` | List of hosts checked out by this scenario | +| User variables | Any variable from `variables` section or CLI | +| Captured variables | Any variable captured via `capture` | + +### Step Memory Attributes + +Access previous step results: + +```yaml +- name: Run command + action: ssh + arguments: + command: "ls /root" + with: + hosts: scenario_inventory + capture: + as: ls_result + +- name: Check if file exists + action: ssh + arguments: + command: "cat /root/config.yaml" + with: + hosts: scenario_inventory + when: "'config.yaml' in ls_result.stdout" +``` + +Step memory has these attributes: +- `name` - Step name +- `output` - Action result +- `status` - "pending", "running", "completed", "skipped", or "failed" + +--- + +## Host Selection and Inventory Filters + +### Inventory References + +| Expression | Description | +|------------|-------------| +| `scenario_inventory` | All hosts checked out by this scenario | +| `inventory` | All hosts from main Broker inventory | +| `@scenario_inv` | Scenario inventory (for filtering) | +| `@inv` | Main inventory (for filtering) | + +### Filter Expressions + +Filter hosts using Python-like expressions: + +```yaml +# Index access +"@scenario_inv[0]" # First host +"@scenario_inv[-1]" # Last host + +# Slicing +"@scenario_inv[0:3]" # First three hosts +"@scenario_inv[1:]" # All except first +"@inv[:]" # All hosts (as list) + +# Attribute filtering +"'rhel' in @inv.name" # Hosts with 'rhel' in name +"'satellite' in @inv.hostname" # Hosts with 'satellite' in hostname +"@inv._broker_provider == 'AnsibleTower'" # By provider +``` + +--- + +## Loops + +Execute a step multiple times over an iterable. + +### Basic Loop + +```yaml +- name: Process each host + action: ssh + arguments: + command: "hostname" + loop: + iterable: "@scenario_inv[:]" # Loop over all scenario hosts + iter_var: current_host + with: + hosts: "{{ current_host }}" # Use loop variable +``` + +### Loop Over Variables + +```yaml +variables: + PACKAGES: + - httpd + - postgresql + - redis + +steps: + - name: Install packages + action: ssh + arguments: + command: "dnf install -y {{ package }}" + loop: + iterable: PACKAGES + iter_var: package + with: + hosts: scenario_inventory +``` + +### Loop with Dictionary Items + +Use tuple unpacking to iterate over dictionary items: + +```yaml +- name: Get command output from each host + action: ssh + arguments: + command: "uptime" + with: + hosts: scenario_inventory + capture: + as: uptime_results # Dict: {hostname: Result} + +- name: Process each result + action: output + arguments: + content: "{{ hostname }}: {{ result.stdout }}" + destination: stdout + loop: + iterable: uptime_results.items() + iter_var: hostname, result # Tuple unpacking +``` + +### Loop with Conditional + +The `when` condition is evaluated for each iteration: + +```yaml +- name: Only process successful results + action: output + arguments: + content: "{{ hostname }} is healthy" + loop: + iterable: check_results.items() + iter_var: hostname, result + when: result.status == 0 +``` + +### Loop Error Handling + +Continue loop even if some iterations fail: + +```yaml +- name: Run risky command on each host + action: ssh + arguments: + command: "risky-operation" + loop: + iterable: "@scenario_inv[:]" + iter_var: host + on_error: continue # Don't stop on failure + with: + hosts: "{{ host }}" +``` + +--- + +## Error Handling + +### Simple Continue + +Proceed to next step even if current step fails: + +```yaml +- name: Optional step + action: ssh + arguments: + command: "optional-command" + with: + hosts: scenario_inventory + on_error: continue +``` + +### Recovery Steps + +Execute cleanup or recovery actions when a step fails: + +```yaml +- name: Critical operation + action: ssh + arguments: + command: "critical-operation" + with: + hosts: scenario_inventory + on_error: + - name: Log failure + action: output + arguments: + content: "Critical operation failed, cleaning up..." + destination: stderr + + - name: Cleanup resources + action: checkin + with: + hosts: scenario_inventory + + - name: Exit with error + action: exit + arguments: + return_code: 1 + message: "Critical operation failed" +``` + +### Exit on Error Control + +For non-critical steps without recovery: + +```yaml +- name: Try to gather metrics + action: ssh + arguments: + command: "collect-metrics" + with: + hosts: scenario_inventory + exit_on_error: false # Continue regardless of outcome +``` + +--- + +## Capturing Output + +Store step results in variables for later use. + +### Basic Capture + +```yaml +- name: Get hostname + action: ssh + arguments: + command: "hostname -f" + with: + hosts: scenario_inventory + capture: + as: hostname_result + +- name: Display hostname + action: output + arguments: + content: "FQDN: {{ hostname_result.stdout }}" +``` + +### Capture with Transform + +Extract or transform the output before storing: + +```yaml +- name: Get OS version + action: ssh + arguments: + command: "cat /etc/redhat-release" + with: + hosts: scenario_inventory + capture: + as: os_version + transform: "{{ step.output.stdout.strip() }}" +``` + +### Capture in Loops + +When capturing loop results, each iteration's result is stored in a dictionary: + +```yaml +- name: Check each service + action: ssh + arguments: + command: "systemctl is-active {{ service }}" + loop: + iterable: SERVICES + iter_var: service + with: + hosts: scenario_inventory + capture: + as: service_status # Dict: {"httpd": Result, "nginx": Result, ...} +``` + +Use custom keys for better organization: + +```yaml +- name: Get workflow details + action: provider_info + arguments: + provider: AnsibleTower + query: + workflow: "{{ workflow_name }}" + loop: + iterable: workflow_list + iter_var: workflow_name + capture: + as: workflow_details + key: result.name # Use workflow name as dict key +``` + +--- + +## Complete Examples + +### Example 1: CI/CD Pipeline Test + +```yaml +# ci_pipeline_test.yaml +# Tests deployment and runs verification on containers + +variables: + TEST_IMAGE: ubi9 + HOST_COUNT: 2 + TEST_PACKAGES: + - python3 + - git + - make + +config: + inventory_path: ~/.broker/ci_test_inventory.yaml + +steps: + - name: Provision test containers + action: checkout + arguments: + container_host: "{{ TEST_IMAGE }}" + count: "{{ HOST_COUNT }}" + + - name: Install test dependencies + action: ssh + arguments: + command: "dnf install -y {{ TEST_PACKAGES | join(' ') }}" + with: + hosts: scenario_inventory + parallel: true + + - name: Clone test repository + action: ssh + arguments: + command: "git clone https://github.com/example/tests.git /root/tests" + with: + hosts: scenario_inventory + + - name: Run tests + action: ssh + arguments: + command: "cd /root/tests && make test" + timeout: 300 + with: + hosts: scenario_inventory + capture: + as: test_results + + - name: Save test results + action: output + arguments: + content: "{{ test_results }}" + destination: ./test_results.yaml + + - name: Cleanup containers + action: checkin + with: + hosts: scenario_inventory +``` + +### Example 2: Multi-Provider Inventory Management + +```yaml +# sync_and_report.yaml +# Syncs inventory from multiple providers and generates a report + +variables: + PROVIDERS: + - AnsibleTower + - Container + - Beaker + +steps: + - name: Sync all provider inventories + action: inventory + arguments: + sync: "{{ provider }}" + loop: + iterable: PROVIDERS + iter_var: provider + on_error: continue + capture: + as: sync_results + + - name: Load full inventory + action: inventory + arguments: {} + capture: + as: full_inventory + + - name: Generate inventory report + action: output + arguments: + content: | + # Broker Inventory Report + Generated: {{ now }} + Total hosts: {{ full_inventory | length }} + + Hosts by provider: + {% for host in full_inventory %} + - {{ host.hostname }} ({{ host._broker_provider }}) + {% endfor %} + destination: ./inventory_report.md + + - name: Display summary + action: output + arguments: + content: "Synced {{ PROVIDERS | length }} providers. Total hosts: {{ full_inventory | length }}" +``` + +### Example 3: Deployment with Rollback + +```yaml +# deploy_with_rollback.yaml +# Deploys application with automatic rollback on failure + +variables: + WORKFLOW: deploy-application + APP_VERSION: "2.1.0" + ROLLBACK_VERSION: "2.0.0" + +steps: + - name: Deploy new version + action: checkout + arguments: + workflow: "{{ WORKFLOW }}" + app_version: "{{ APP_VERSION }}" + on_error: + - name: Log deployment failure + action: output + arguments: + content: "Deployment of {{ APP_VERSION }} failed, initiating rollback..." + destination: stderr + + - name: Deploy rollback version + action: checkout + arguments: + workflow: "{{ WORKFLOW }}" + app_version: "{{ ROLLBACK_VERSION }}" + on_error: + - name: Critical failure + action: exit + arguments: + return_code: 2 + message: "Both deployment and rollback failed!" + + - name: Rollback successful + action: output + arguments: + content: "Rollback to {{ ROLLBACK_VERSION }} successful" + + - name: Exit with warning + action: exit + arguments: + return_code: 1 + message: "Deployed rollback version due to failure" + + - name: Verify deployment + action: ssh + arguments: + command: "curl -s localhost:8080/health" + with: + hosts: scenario_inventory + capture: + as: health_check + + - name: Deployment complete + action: output + arguments: + content: "Successfully deployed {{ APP_VERSION }}" +``` + +--- + +## CLI Reference + +### List Available Scenarios + +```bash +broker scenarios list +``` + +Shows all scenarios in `~/.broker/scenarios/`. + +### Execute a Scenario + +```bash +# By name (from scenarios directory) +broker scenarios execute my_scenario + +# By path +broker scenarios execute /path/to/scenario.yaml + +# With variable overrides +broker scenarios execute my_scenario --MY_VAR value --COUNT 5 + +# With config overrides +broker scenarios execute my_scenario --config.settings.Container.runtime docker + +# Run in background +broker scenarios execute my_scenario --background +``` + +### Get Scenario Information + +```bash +broker scenarios info my_scenario + +# Without syntax highlighting +broker scenarios info my_scenario --no-syntax +``` + +### Validate a Scenario + +```bash +broker scenarios validate my_scenario +``` + +Checks syntax and schema validation without executing. + +--- + +## Tips and Best Practices + +1. **Always clean up**: Include a `checkin` step at the end of your scenarios, preferably with error handling to ensure cleanup even on failure. + +2. **Use meaningful names**: Step names appear in logs and can be referenced via `steps['Step Name']`. + +3. **Capture intermediate results**: Use `capture` liberally to store results for debugging and conditional logic. + +4. **Test with `--background`**: Long-running scenarios can be run in the background. + +5. **Validate before running**: Use `broker scenarios validate` to catch syntax errors early. + +6. **Modularize complex workflows**: Split large scenarios into smaller ones and use `run_scenarios` to chain them. + +7. **Use variables for flexibility**: Define configurable values in the `variables` section so they can be overridden via CLI. + +8. **Handle errors gracefully**: Use `on_error` blocks for critical steps that need cleanup on failure. + +--- + +## Troubleshooting + +### Common Issues + +**"Undefined variable in template"** +- Check that the variable is defined in `variables` section or captured by a previous step +- Variables are case-sensitive +- Use `broker scenarios info` to see available variables + +**"Scenario not found"** +- Ensure file is in `~/.broker/scenarios/` or provide full path +- File must have `.yaml` or `.yml` extension + +**"SSH action requires target hosts"** +- Add a `with.hosts` specification to the step +- Ensure a previous `checkout` step has added hosts to `scenario_inventory` + +**"Step failed but no on_error defined"** +- Add `on_error: continue` to ignore failures +- Add `exit_on_error: false` to continue without error handling +- Add an `on_error` block with recovery steps + +### Debugging + +Enable verbose logging: +```bash +broker --log-level debug scenarios execute my_scenario +``` + +Check the scenario structure: +```bash +broker scenarios info my_scenario +``` + +Validate without executing: +```bash +broker scenarios validate my_scenario +``` diff --git a/tests/functional/scenarios/comprehensive_container_test.yaml b/tests/functional/scenarios/comprehensive_container_test.yaml new file mode 100644 index 00000000..fe350fc1 --- /dev/null +++ b/tests/functional/scenarios/comprehensive_container_test.yaml @@ -0,0 +1,415 @@ +# Comprehensive Container Provider Test Scenario +# +# This scenario exercises the full breadth of Broker scenarios functionality +# using the Container provider. It demonstrates: +# +# - checkout with count and configuration +# - provider_info queries (flag-style and value-style) +# - ssh commands with capture and transforms +# - scp file uploads +# - sftp operations (upload and download) +# - output to stdout, stderr, and files (JSON/YAML) +# - loops with inventory filters, dict.items(), and variable lists +# - when conditions (simple and complex) +# - on_error recovery steps +# - exit_on_error: false for non-critical steps +# - parallel vs sequential execution +# - capture with custom keys +# - variables with CLI override capability +# - config section with provider settings +# +# Prerequisites: +# - A running container runtime (Podman or Docker) +# - Container provider configured in broker_settings.yaml +# - A broker-compatible container image (e.g., ubi8 with SSH) +# +# Usage: +# broker scenarios execute comprehensive_container_test +# broker scenarios execute comprehensive_container_test --CONTAINER_IMAGE ubi9 --HOST_COUNT 3 +# +# Note: This scenario creates temporary files in /tmp and cleans up after itself. + +config: + # Custom inventory path for this scenario's hosts + inventory_path: ~/.broker/comprehensive_test_inventory.yaml + # Custom log file for this scenario (demonstrates filename-only resolution) + log_path: comprehensive_container_test.log + # Provider settings (can be overridden via CLI with --config.settings.Container.runtime) + settings: + Container: + # runtime: podman # Uncomment to override default + auto_map_ports: true + +variables: + # Container configuration - override via CLI as needed + CONTAINER_IMAGE: localhost/ubi8:latest + HOST_COUNT: 2 + + # Test configuration + TEST_COMMANDS: + - "cat /etc/os-release" + - "uname -a" + - "df -h" + + TEST_FILENAME: broker_test_file.txt + TEST_CONTENT: "Hello from Broker Scenarios!" + + # Feature flags for conditional execution + RUN_SFTP_TESTS: true + RUN_PARALLEL_TESTS: true + VERBOSE_OUTPUT: false + +steps: + # ============================================================================ + # SECTION 1: Provider Information Queries + # ============================================================================ + + - name: Query available container images (flag-style) + action: provider_info + arguments: + provider: Container + query: container_hosts + capture: + as: available_images + + - name: Display available images + action: output + arguments: + content: "Found {{ available_images | length }} broker-compatible container images" + destination: stdout + + - name: Query specific image details (value-style) + action: provider_info + arguments: + provider: Container + query: + container_host: "{{ CONTAINER_IMAGE }}" + capture: + as: image_details + on_error: continue # Continue if image not found (will use default) + + - name: Log image details if found + action: output + arguments: + content: "Using image: {{ image_details.name if image_details else CONTAINER_IMAGE }}" + destination: stdout + when: image_details is defined + + # ============================================================================ + # SECTION 2: Container Checkout + # ============================================================================ + + - name: Provision test containers + action: checkout + arguments: + container_host: "{{ CONTAINER_IMAGE }}" + count: "{{ HOST_COUNT }}" + on_error: + - name: Log checkout failure + action: output + arguments: + content: "ERROR: Failed to checkout containers. Check container runtime status." + destination: stderr + - name: Exit on checkout failure + action: exit + arguments: + return_code: 10 + message: "Container checkout failed - cannot continue tests" + + - name: Verify checkout succeeded + action: output + arguments: + content: "Successfully provisioned {{ scenario_inventory | length }} container(s)" + destination: stdout + + # ============================================================================ + # SECTION 3: Basic SSH Commands with Capture + # ============================================================================ + + - name: Get container hostnames + action: ssh + arguments: + command: "hostname" + with: + hosts: scenario_inventory + capture: + as: hostnames + + - name: Display hostnames (single vs multi-host handling) + action: output + arguments: + content: "Container hostnames: {{ hostnames }}" + destination: stdout + + - name: Run multiple test commands via loop + action: ssh + arguments: + command: "{{ cmd }}" + loop: + iterable: TEST_COMMANDS + iter_var: cmd + on_error: continue # Continue loop even if a command fails + with: + hosts: "@scenario_inv[0]" # Only first host for this test + capture: + as: command_outputs + key: cmd # Use command as dictionary key + + - name: Log verbose command output + action: output + arguments: + content: | + Command outputs from first container: + {{ command_outputs }} + destination: stdout + when: VERBOSE_OUTPUT == true + + # ============================================================================ + # SECTION 4: File Operations (SCP and SFTP) + # ============================================================================ + + - name: Create test file on first container + action: ssh + arguments: + command: "echo '{{ TEST_CONTENT }}' > /tmp/{{ TEST_FILENAME }}" + with: + hosts: "@scenario_inv[0]" + + - name: Use SSH to copy file between containers (simulating SCP workflow) + action: ssh + arguments: + command: "echo 'SCP simulation - file created via SSH' > /tmp/scp_test_file.txt" + with: + hosts: scenario_inventory + capture: + as: scp_simulation + exit_on_error: false + + - name: Verify file creation on each host + action: ssh + arguments: + command: "cat /tmp/scp_test_file.txt 2>/dev/null || echo 'File not found'" + with: + hosts: scenario_inventory + capture: + as: file_verification + + # SFTP tests - conditional on RUN_SFTP_TESTS variable + # Note: SFTP requires local files. We'll test by creating a file remotely + # and then demonstrating the SFTP read capability + - name: Create files for SFTP test on containers + action: ssh + arguments: + command: "echo 'SFTP test content from $(hostname)' > /tmp/sftp_test_file.txt" + with: + hosts: scenario_inventory + when: RUN_SFTP_TESTS == true + + - name: Read remote file content (SFTP read simulation via SSH) + action: ssh + arguments: + command: "cat /tmp/sftp_test_file.txt" + with: + hosts: "@scenario_inv[0]" + capture: + as: sftp_read_result + when: RUN_SFTP_TESTS == true + exit_on_error: false + + # ============================================================================ + # SECTION 5: Parallel vs Sequential Execution + # ============================================================================ + + - name: Parallel command execution (default) + action: ssh + arguments: + command: "sleep 1 && echo 'Parallel execution from $(hostname)'" + timeout: 10 + with: + hosts: scenario_inventory + parallel: true # This is the default + capture: + as: parallel_results + when: RUN_PARALLEL_TESTS == true + + - name: Sequential command execution + action: ssh + arguments: + command: "echo 'Sequential execution from $(hostname)'" + with: + hosts: scenario_inventory + parallel: false # Force sequential + capture: + as: sequential_results + + # ============================================================================ + # SECTION 6: Loop with Dictionary Items and Tuple Unpacking + # ============================================================================ + + - name: Process results from each host + action: output + arguments: + content: "Host {{ hostname }} returned status {{ result.status }}" + destination: stdout + loop: + iterable: sequential_results.items() + iter_var: hostname, result + when: sequential_results is defined and sequential_results is mapping + + # ============================================================================ + # SECTION 7: Complex Conditionals and Error Handling + # ============================================================================ + + - name: Check disk space (non-critical) + action: ssh + arguments: + command: "df -h / | tail -1 | awk '{print $5}' | tr -d '%'" + with: + hosts: "@scenario_inv[0]" # Single host for simpler output + capture: + as: disk_usage + exit_on_error: false + + - name: Log disk usage + action: output + arguments: + content: "Disk usage on first container: {{ disk_usage.stdout if disk_usage else 'unknown' }}%" + destination: stdout + when: disk_usage is defined + + - name: Intentionally failing command (demonstrates on_error recovery) + action: ssh + arguments: + command: "exit 1" # This will fail + with: + hosts: "@scenario_inv[0]" + on_error: + - name: Log expected failure + action: output + arguments: + content: "Expected failure handled - recovery steps executed successfully" + destination: stdout + # Note: We don't exit here, allowing the scenario to continue + + # ============================================================================ + # SECTION 8: Output to Files (JSON and YAML formats) + # ============================================================================ + + - name: Collect final test summary + action: ssh + arguments: + command: "uptime" + with: + hosts: scenario_inventory + capture: + as: uptime_results + + - name: Save results to JSON file + action: output + arguments: + content: + scenario_name: comprehensive_container_test + containers_tested: "{{ scenario_inventory | length }}" + image_used: "{{ CONTAINER_IMAGE }}" + test_commands_run: "{{ TEST_COMMANDS }}" + sftp_tests_enabled: "{{ RUN_SFTP_TESTS }}" + parallel_tests_enabled: "{{ RUN_PARALLEL_TESTS }}" + destination: /tmp/broker_scenario_results.json + mode: overwrite + + - name: Append summary to log file + action: output + arguments: + content: "Test run completed with {{ scenario_inventory | length }} containers" + destination: /tmp/broker_scenario_log.txt + mode: append + + # Note: We'll save the inventory snapshot after syncing, using the dict format + # from the inventory action rather than Host objects + + # ============================================================================ + # SECTION 9: Inventory Operations + # ============================================================================ + + - name: Sync main broker inventory + action: inventory + arguments: + sync: Container + capture: + as: synced_inventory + exit_on_error: false # Don't fail if sync has issues + + - name: Save inventory snapshot to YAML (using dict format from inventory sync) + action: output + arguments: + content: "{{ synced_inventory }}" + destination: /tmp/broker_test_inventory_snapshot.yaml + when: synced_inventory is defined + + - name: Display inventory count + action: output + arguments: + content: "Main broker inventory sync completed" + destination: stdout + when: synced_inventory is defined + + - name: Note if inventory sync failed + action: output + arguments: + content: "Note: Inventory sync was skipped or failed (this is expected in some environments)" + destination: stdout + when: synced_inventory is not defined + + # ============================================================================ + # SECTION 10: Cleanup with Error Recovery + # ============================================================================ + + - name: Cleanup test files on containers + action: ssh + arguments: + command: "rm -f /tmp/broker_test_* /tmp/scp_test_* /tmp/sftp_test_* 2>/dev/null; echo 'Cleanup complete'" + with: + hosts: scenario_inventory + exit_on_error: false + + - name: Final status output + action: output + arguments: + content: | + + ============================================ + COMPREHENSIVE SCENARIO TEST COMPLETE + ============================================ + Containers tested: {{ scenario_inventory | length }} + Image: {{ CONTAINER_IMAGE }} + Results saved to: /tmp/broker_scenario_results.json + ============================================ + destination: stdout + + - name: Release all containers + action: checkin + with: + hosts: scenario_inventory + on_error: + - name: Log checkin failure + action: output + arguments: + content: "WARNING: Failed to release some containers. Manual cleanup may be required." + destination: stderr + - name: List remaining containers + action: inventory + arguments: + sync: Container + capture: + as: remaining_inventory + - name: Exit with warning + action: exit + arguments: + return_code: 5 + message: "Scenario completed but container cleanup failed" + + - name: Confirm cleanup + action: output + arguments: + content: "All containers successfully released. Scenario complete!" + destination: stdout diff --git a/tests/functional/scenarios/deploy_checkin_containers.yaml b/tests/functional/scenarios/deploy_checkin_containers.yaml new file mode 100644 index 00000000..43b28b84 --- /dev/null +++ b/tests/functional/scenarios/deploy_checkin_containers.yaml @@ -0,0 +1,19 @@ +steps: + - name: Discover container hosts + action: provider_info + arguments: + provider: Container + query: container_hosts + capture: + as: container_hosts + - name: Deploy each container host + action: checkout + arguments: + container_host: "{{ c_host }}" + loop: + iterable: container_hosts + iter_var: c_host + - name: Checkin all deployed containers + action: checkin + with: + hosts: scenario_inventory diff --git a/tests/functional/scenarios/deploy_details.yaml b/tests/functional/scenarios/deploy_details.yaml new file mode 100644 index 00000000..392780d5 --- /dev/null +++ b/tests/functional/scenarios/deploy_details.yaml @@ -0,0 +1,29 @@ +steps: + - name: List workflows + action: provider_info + arguments: + provider: AnsibleTower + query: workflows + capture: + as: workflow_list + + # Value-style query + - name: Get workflow details + action: provider_info + arguments: + provider: AnsibleTower + query: + workflow: "{{ workflow_name }}" + when: "'deploy' in workflow_name" + loop: + iterable: workflow_list + iter_var: workflow_name + capture: + as: workflow_details + key: workflow_name + + - name: Output results per workflow + action: output + arguments: + content: workflow_details + destination: deploy_workflows.json diff --git a/tests/functional/test_scenarios.py b/tests/functional/test_scenarios.py new file mode 100644 index 00000000..52625140 --- /dev/null +++ b/tests/functional/test_scenarios.py @@ -0,0 +1,27 @@ +from pathlib import Path +from broker.scenarios import ScenarioRunner + +TEST_SCENARIO_DIR = Path(__file__).parent / "scenarios" + +def test_comprehensive_container_scenario(): + scenario = ScenarioRunner(scenario_path=TEST_SCENARIO_DIR / "comprehensive_container_test.yaml") + + assert scenario is not None + assert scenario.scenario_name == "comprehensive_container_test" + assert scenario.config.get("inventory_path") == "~/.broker/comprehensive_test_inventory.yaml" + assert scenario.config.get("log_path") == "comprehensive_container_test.log" + scenario.run() + +def test_deploy_checkin_containers_scenario(): + scenario = ScenarioRunner(scenario_path=TEST_SCENARIO_DIR / "deploy_checkin_containers.yaml") + + assert scenario is not None + assert scenario.scenario_name == "deploy_checkin_containers" + scenario.run() + +def test_deploy_details_scenario(): + scenario = ScenarioRunner(scenario_path=TEST_SCENARIO_DIR / "deploy_details.yaml") + + assert scenario is not None + assert scenario.scenario_name == "deploy_details" + scenario.run() \ No newline at end of file From 1d9278b645ca0faa3f0fb0005e1470aa131d2a6a Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Mon, 15 Dec 2025 16:55:10 -0500 Subject: [PATCH 10/10] Add comprehensive testing for scenario runner features Tests: - Introduce `test_scenarios.py` with extensive unit tests for the `broker.scenarios` module. - Cover scenario loading, schema validation, Jinja2 templating, expression evaluation, and conditional logic. - Verify support for loop iteration, output capture, and argument mapping within scenario steps. - Add specific sample scenario YAML files (`capture_scenario.yaml`, `conditions_scenario.yaml`, `loop_scenario.yaml`, `invalid_schema.yaml`, `valid_scenario.yaml`) to exercise various scenario features and validation rules. - Test `StepMemory` functionality and `ScenarioRunner` initialization, configuration, and built-in actions (`output`, `exit`). Configuration: - Add `jinja2` and `jsonschema` as core dependencies to `pyproject.toml` to enable scenario templating and schema validation. - Update `tox.toml` to include the new `test_scenarios.py` in the test suite. --- broker/commands.py | 49 +- broker/exceptions.py | 25 + broker/helpers/dict_utils.py | 2 +- broker/helpers/file_utils.py | 2 +- broker/scenarios.py | 132 +- broker/settings.py | 9 +- pyproject.toml | 2 + scenarios/scenario_schema.json | 155 --- scenarios/scenarios_tutorial.md | 1068 ----------------- tests/data/scenarios/capture_scenario.yaml | 18 + tests/data/scenarios/conditions_scenario.yaml | 27 + tests/data/scenarios/invalid_schema.yaml | 8 + tests/data/scenarios/loop_scenario.yaml | 28 + tests/data/scenarios/valid_scenario.yaml | 21 + .../comprehensive_container_test.yaml | 2 +- tests/functional/test_scenarios.py | 5 +- tests/test_scenarios.py | 397 ++++++ tox.toml | 1 + 18 files changed, 692 insertions(+), 1259 deletions(-) delete mode 100644 scenarios/scenario_schema.json delete mode 100644 scenarios/scenarios_tutorial.md create mode 100644 tests/data/scenarios/capture_scenario.yaml create mode 100644 tests/data/scenarios/conditions_scenario.yaml create mode 100644 tests/data/scenarios/invalid_schema.yaml create mode 100644 tests/data/scenarios/loop_scenario.yaml create mode 100644 tests/data/scenarios/valid_scenario.yaml create mode 100644 tests/test_scenarios.py diff --git a/broker/commands.py b/broker/commands.py index d871dbc6..4c2c2527 100644 --- a/broker/commands.py +++ b/broker/commands.py @@ -69,11 +69,17 @@ def wrapper(*args, **kwargs): helpers.emit(return_code=0) return retval except Exception as err: # noqa: BLE001 -- we want to catch all exceptions - if not isinstance(err, exceptions.BrokerError): + if isinstance(err, exceptions.ScenarioError): + # Show full message for scenario errors since context is important + logger.error(f"Scenario failed: {err.message}") + CONSOLE.print(f"[red]Scenario failed:[/red] {err.message}") + elif not isinstance(err, exceptions.BrokerError): err = exceptions.BrokerError(err) logger.error(f"Command failed: {err.message}") + CONSOLE.print(f"[red]Command failed:[/red] {err.message}") else: # BrokerError children already log their messages logger.error(f"Command failed due to: {type(err).__name__}") + CONSOLE.print(f"[red]Command failed due to:[/red] {type(err).__name__}") helpers.emit(return_code=err.error_code, error_message=str(err.message)) sys.exit(err.error_code) @@ -220,6 +226,7 @@ def cli(version): table.add_column("Location", justify="left", style="magenta") table.add_row("Broker Directory", str(settings.BROKER_DIRECTORY.absolute())) + table.add_row("Scenarios Directory", f"{settings.BROKER_DIRECTORY.absolute()}/scenarios") table.add_row("Settings File", str(settings.settings_path.absolute())) table.add_row("Inventory File", f"{settings.BROKER_DIRECTORY.absolute()}/inventory.yaml") table.add_row("Log File", f"{settings.BROKER_DIRECTORY.absolute()}/logs/broker.log") @@ -315,7 +322,13 @@ def checkin(hosts, background, all_, sequential, filter): unmatched.discard(host.get("name")) if unmatched: - logger.warning(f"The following hosts were not found in inventory: {', '.join(unmatched)}") + logger.warning( + "The following hosts were not found in inventory: %s", + ", ".join(unmatched), + ) + CONSOLE.print( + f"[yellow]Warning:[/yellow] The following hosts were not found in inventory: {', '.join(unmatched)}" + ) if to_remove: Broker(hosts=to_remove).checkin(sequential=sequential) @@ -455,6 +468,7 @@ def execute(ctx, background, nick, output_format, artifacts, args_file, provider click.echo(result) elif output_format == "log": logger.info(result) + CONSOLE.print(result) elif output_format == "yaml": click.echo(helpers.yaml_format(result)) @@ -567,8 +581,10 @@ def validate(chunk): try: ConfigManager(settings.settings_path).validate(chunk, PROVIDERS) logger.info("Validation passed!") + CONSOLE.print("[green]Validation passed![/green]") except exceptions.BrokerError as err: logger.warning(f"Validation failed: {err}") + CONSOLE.print(f"[yellow]Validation failed:[/yellow] {err}") # --- Scenarios CLI Group --- @@ -595,10 +611,9 @@ def scenarios_list(): table = Table(title="Available Scenarios") table.add_column("Name", style="cyan") - table.add_column("Path", style="magenta") for name in scenario_names: - table.add_row(name, str(SCENARIOS_DIR / f"{name}.yaml")) + table.add_row(name) CONSOLE.print(table) @@ -646,7 +661,25 @@ def scenarios_execute(ctx, scenario, background): cli_vars=cli_vars, cli_config=cli_config, ) - runner.run() + try: + runner.run() + finally: + # Display scenario inventory if any hosts remain + if runner.scenario_inventory: + inv_data = [host.to_dict() for host in runner.scenario_inventory] + curated_host_info = [ + helpers.inventory_fields_to_dict( + inventory_fields=settings.settings.inventory_fields, + host_dict=host, + provider_actions=PROVIDER_ACTIONS, + ) + for host in inv_data + ] + table = helpers.dictlist_to_table( + curated_host_info, "Scenario Inventory (hosts still checked out)", _id=True + ) + CONSOLE.print(table) + CONSOLE.print(f"[dim]Inventory file: {runner.inventory_path}[/dim]") @guarded_command(group=scenarios) @@ -683,11 +716,11 @@ def scenarios_validate(scenario): is_valid, error_msg = validate_scenario(scenario_path) if is_valid: - logger.info(f"Scenario '{scenario}' is valid!") + CONSOLE.print(f"[green]Scenario '{scenario}' is valid![/green]") if error_msg: # Schema not found message - logger.warning(error_msg) + CONSOLE.print(f"[yellow]Warning:[/yellow] {error_msg}") else: - logger.error(f"Scenario '{scenario}' is invalid: {error_msg}") + CONSOLE.print(f"[red]Scenario '{scenario}' is invalid:[/red] {error_msg}") def _make_shell_help_func(cmd, shell_instance): diff --git a/broker/exceptions.py b/broker/exceptions.py index 102962ce..ba756858 100644 --- a/broker/exceptions.py +++ b/broker/exceptions.py @@ -97,3 +97,28 @@ class ScenarioError(BrokerError): """Raised when a problem occurs during scenario execution.""" error_code = 16 + + def __init__(self, message="Scenario execution failed", step_name=None, scenario_name=None): + """Initialize ScenarioError with optional context. + + Args: + message: The error message + step_name: Name of the step where the error occurred (optional) + scenario_name: Name of the scenario being executed (optional) + """ + self.step_name = step_name + self.scenario_name = scenario_name + + # Build a contextual message + parts = [] + if scenario_name: + parts.append(f"Scenario '{scenario_name}'") + if step_name: + parts.append(f"step '{step_name}'") + + if parts: + self.message = f"{' '.join(parts)}: {message}" + else: + self.message = message + + super().__init__(message=self.message) diff --git a/broker/helpers/dict_utils.py b/broker/helpers/dict_utils.py index 115fa892..a54620ec 100644 --- a/broker/helpers/dict_utils.py +++ b/broker/helpers/dict_utils.py @@ -17,7 +17,7 @@ def merge_dicts(dict1, dict2): :return: merged dictionary """ if not isinstance(dict1, MutableMapping) or not isinstance(dict2, MutableMapping): - return dict2 + return dict2 if dict2 is not None else dict1 dict1 = clean_dict(dict1) dict2 = clean_dict(dict2) merged = {} diff --git a/broker/helpers/file_utils.py b/broker/helpers/file_utils.py index ac476e1b..0433f29d 100644 --- a/broker/helpers/file_utils.py +++ b/broker/helpers/file_utils.py @@ -213,7 +213,7 @@ def data_to_tempfile(data, path=None, as_tar=False): @contextmanager def temporary_tar(paths): """Create a temporary tar file and return the path.""" - temp_tar = Path(f"{uuid4().hex[-10]}.tar") + temp_tar = Path(f"{uuid4().hex[-10:]}.tar") with tarfile.open(temp_tar, mode="w") as tar: for path in paths: logger.debug(f"Adding {path.absolute()} to {temp_tar.absolute()}") diff --git a/broker/scenarios.py b/broker/scenarios.py index ef8028b1..377bb10b 100644 --- a/broker/scenarios.py +++ b/broker/scenarios.py @@ -13,6 +13,7 @@ import json import logging from pathlib import Path +import sys import jinja2 import jsonschema @@ -339,7 +340,8 @@ def __init__(self, scenario_path, cli_vars=None, cli_config=None): # Initialize variables (scenario vars, then CLI overrides on top) self.variables = self.data.get("variables", {}).copy() - self.variables.update(self.cli_vars) + # Apply CLI variable overrides with type conversion + self._apply_cli_var_overrides() # Steps memory: mapping step name -> StepMemory self.steps_memory = {} @@ -402,6 +404,76 @@ def _apply_cli_config_overrides(self): target = target[part] target[parts[-1]] = value + def _convert_cli_value(self, cli_value, original_value, var_name): + """Convert a CLI string value to match the type of the original value. + + Args: + cli_value: String value from CLI + original_value: Original value from scenario YAML + var_name: Name of the variable (for logging) + + Returns: + Converted value, or original cli_value if conversion fails + """ + original_type = type(original_value) + + # If original is None or already the right type, keep as-is + if original_value is None or isinstance(cli_value, original_type): + return cli_value + + try: + if original_type is bool: + lower_val = str(cli_value).lower() + bool_map = { + "true": True, + "1": True, + "yes": True, + "on": True, + "false": False, + "0": False, + "no": False, + "off": False, + } + if lower_val in bool_map: + return bool_map[lower_val] + logger.warning( + f"Could not convert CLI value '{cli_value}' to bool for '{var_name}', " + f"keeping as string" + ) + elif original_type is int: + return int(cli_value) + elif original_type is float: + return float(cli_value) + elif original_type in (list, dict): + # For collections, try to parse as JSON if it looks like JSON + if isinstance(cli_value, str) and cli_value and cli_value[0] in ("{", "["): + return json.loads(cli_value) + logger.warning( + f"CLI value for '{var_name}' doesn't appear to be valid {original_type.__name__} JSON, " + f"keeping as string" + ) + except (ValueError, TypeError, IndexError) as e: + logger.warning( + f"Failed to convert CLI value '{cli_value}' to {original_type.__name__} for '{var_name}': {e}. " + f"Keeping as string." + ) + return cli_value + + def _apply_cli_var_overrides(self): + """Apply CLI variable overrides with type conversion. + + CLI variables are passed as strings. This method attempts to convert them + to match the type of the original scenario variable. If the original variable + doesn't exist or conversion fails, the string value is kept with a warning. + """ + for key, cli_value in self.cli_vars.items(): + if key not in self.variables: + # New variable from CLI - keep as string + self.variables[key] = cli_value + else: + # Convert to match original type + self.variables[key] = self._convert_cli_value(cli_value, self.variables[key], key) + def _get_inventory_path(self): """Get the path for the scenario-specific inventory file. @@ -409,7 +481,7 @@ def _get_inventory_path(self): Path object for the inventory file """ if inv_path := self.config.get("inventory_path"): - return Path(inv_path) + return Path(inv_path).expanduser() return BROKER_DIRECTORY / f"scenario_{self.scenario_name}_inventory.yaml" def _setup_logging(self): @@ -585,11 +657,19 @@ def _execute_step(self, step_data, previous_step_memory): # noqa: PLR0912, PLR0 logger.info(f"Executing on_error handler for step '{step_name}'") try: self._execute_steps(on_error) + except SystemExit: + # Let SystemExit pass through (from exit action in error handler) + raise except Exception as handler_err: - logger.error(f"on_error handler also failed: {handler_err}") + # If the error handler itself failed, this is a secondary failure + # Re-raise it so it terminates the scenario raise handler_err from e elif step_data.get("exit_on_error", True): - raise ScenarioError(f"Step '{step_name}' failed and exit_on_error is True") from e + raise ScenarioError( + f"Step failed: {e}", + step_name=step_name, + scenario_name=self.scenario_name, + ) from e else: logger.warning(f"Step '{step_name}' failed but exit_on_error=False, continuing") @@ -652,8 +732,9 @@ def _execute_loop(self, step_data, base_arguments, target_hosts, context): # no # Convert dict to list of tuples for iteration resolved_iterable = list(resolved_iterable.items()) elif not isinstance(resolved_iterable, (list, tuple)): - # Convert iterables like dict_items to a list + # Convert other iterables to a list, avoiding double-conversion of dict.items() try: + # Convert to list once - dict_items, dict_keys, etc will be converted resolved_iterable = list(resolved_iterable) except TypeError: resolved_iterable = [resolved_iterable] @@ -672,10 +753,17 @@ def _execute_loop(self, step_data, base_arguments, target_hosts, context): # no loop_context = context.copy() # Handle tuple unpacking for multiple iter_var names - if len(iter_var_names) > 1 and isinstance(item, (list, tuple)): - for var_name, value in zip(iter_var_names, item): - loop_context[var_name] = value - default_loop_key = str(item[0]) if item else str(item) + if len(iter_var_names) > 1: + if isinstance(item, (list, tuple)) and len(item) >= len(iter_var_names): + for var_name, value in zip(iter_var_names, item): + loop_context[var_name] = value + default_loop_key = str(item[0]) if item else str(item) + else: + raise ScenarioError( + f"Loop item cannot be unpacked: expected {len(iter_var_names)} " + f"values but got {item}. Ensure the iterable contains " + f"tuples/lists with {len(iter_var_names)} elements." + ) else: loop_context[iter_var_names[0]] = item default_loop_key = str(item) @@ -821,7 +909,12 @@ def _action_ssh(self, arguments, hosts, parallel): otherwise a dict mapping hostname to Result objects. """ if not hosts: - raise ScenarioError("SSH action requires target hosts") + raise ScenarioError( + "SSH action requires target hosts. Specify hosts for this step via " + "the 'with' clause (e.g. 'with: { hosts: [...] }') or ensure that " + "the scenario inventory contains hosts from a previous checkout or " + "inventory action." + ) command = arguments.get("command") if not command: @@ -839,7 +932,7 @@ def run_on_host(host): # Multiple hosts: return dict mapping hostname to result if parallel: results = {} - with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + with ThreadPoolExecutor(max_workers=min(len(hosts), 10)) as executor: futures = [executor.submit(run_on_host, h) for h in hosts] for future in as_completed(futures): hostname, result = future.result() @@ -879,14 +972,14 @@ def scp_to_host(host): # Multiple hosts: return dict mapping hostname to result if parallel: results = {} - with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + with ThreadPoolExecutor(max_workers=min(len(hosts), 10)) as executor: futures = [executor.submit(scp_to_host, h) for h in hosts] for future in as_completed(futures): hostname, result = future.result() results[hostname] = result return results else: - return {h.hostname: scp_to_host(h)[1] for h in hosts} + return dict(scp_to_host(h) for h in hosts) def _action_sftp(self, arguments, hosts, parallel): """Handle sftp action. @@ -934,14 +1027,14 @@ def sftp_single(host): # Multiple hosts: return dict mapping hostname to result if parallel: results = {} - with ThreadPoolExecutor(max_workers=len(hosts)) as executor: + with ThreadPoolExecutor(max_workers=min(len(hosts), 10)) as executor: futures = [executor.submit(sftp_on_host, h) for h in hosts] for future in as_completed(futures): hostname, result = future.result() results[hostname] = result return results else: - return {h.hostname: sftp_on_host(h)[1] for h in hosts} + return dict(sftp_on_host(h) for h in hosts) def _action_execute(self, step_name, arguments): """Handle execute action (provider action).""" @@ -996,8 +1089,6 @@ def _action_output(self, arguments): Returns: The content that was written """ - import sys - content = arguments.get("content") if content is None: raise ScenarioError("Output action requires 'content' argument") @@ -1146,11 +1237,14 @@ def run(self): # Normal exit from exit action logger.info(f"Scenario '{self.scenario_name}' exited with code {e.code}") if e.code != 0: - raise ScenarioError(f"Scenario exited with non-zero code: {e.code}") + raise ScenarioError( + f"Exited with non-zero code: {e.code}", + scenario_name=self.scenario_name, + ) except ScenarioError: raise except Exception as e: - raise ScenarioError(f"Scenario failed: {e}") from e + raise ScenarioError(f"Unexpected error: {e}", scenario_name=self.scenario_name) from e def get_info(self): """Get summary information about the scenario. diff --git a/broker/settings.py b/broker/settings.py index 00f2633f..fdc35795 100644 --- a/broker/settings.py +++ b/broker/settings.py @@ -105,14 +105,13 @@ def _create_and_configure_settings(file_path, file_exists, config_dict): # Add any configuration values passed in, merging nested dicts if config_dict: for key, value in config_dict.items(): - # Normalize key to uppercase for settings lookup - upper_key = key.upper() - existing = new_settings.get(upper_key) + # Use the original key for settings lookup and assignment + existing = new_settings.get(key) if existing is not None and isinstance(existing, dict) and isinstance(value, dict): # Deep merge the nested dictionaries - new_settings[upper_key] = merge_dicts(existing, value) + new_settings[key] = merge_dicts(existing, value) else: - new_settings[upper_key] = value + new_settings[key] = value return new_settings diff --git a/pyproject.toml b/pyproject.toml index 21ac4d5e..7a92e987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ classifiers = [ dependencies = [ "click", "dynaconf>=3.1.6,<4.0.0", + "jinja2", + "jsonschema", "packaging", "python-json-logger", "requests", diff --git a/scenarios/scenario_schema.json b/scenarios/scenario_schema.json deleted file mode 100644 index 5f1e14dc..00000000 --- a/scenarios/scenario_schema.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Scenario Schema", - "description": "A schema for validating scenario YAML files based on the defined specification.", - "type": "object", - "properties": { - "config": { - "type": "object", - "description": "Global configuration settings for the scenario execution.", - "properties": { - "inventory_path": { - "type": "string", - "description": "The file path to the scenario's dedicated inventory file." - }, - "settings": { - "type": "object", - "description": "A nested map of provider-specific settings.", - "additionalProperties": { - "type": "object" - } - } - }, - "additionalProperties": false - }, - "variables": { - "type": "object", - "description": "A key-value map of variables to be used within the steps.", - "additionalProperties": { - "type": ["string", "number", "boolean", "array", "object"] - } - }, - "steps": { - "type": "array", - "description": "A list of step objects that define the scenario's actions.", - "items": { - "$ref": "#/definitions/step" - } - } - }, - "required": [ - "steps" - ], - "additionalProperties": false, - "definitions": { - "step": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "A human-readable name for the step." - }, - "action": { - "type": "string", - "description": "The type of action to perform.", - "enum": [ - "checkout", - "checkin", - "inventory", - "ssh", - "scp", - "sftp", - "execute", - "run_scenarios", - "exit", - "output", - "provider_info" - ] - }, - "arguments": { - "type": "object", - "description": "An optional key-value map of arguments specific to the chosen action." - }, - "with": { - "type": "object", - "description": "Specifies the target hosts for the action.", - "properties": { - "hosts": { - "type": "string", - "description": "Can be 'scenario_inventory' or an inventory filter expression." - } - }, - "required": ["hosts"], - "additionalProperties": false - }, - "when": { - "type": "string", - "description": "A conditional expression that must evaluate to true for the step to run." - }, - "loop": { - "type": "object", - "description": "Defines a loop to run the action multiple times.", - "properties": { - "iterable": { - "type": "string", - "description": "An inventory filter, variable name, or expression that resolves to an iterable. Supports dict methods like 'my_dict.items()' for key-value iteration." - }, - "iter_var": { - "type": "string", - "description": "The name(s) of the variable(s) to hold the current item. Supports tuple unpacking with comma-separated names (e.g., 'key, value' for dict items)." - }, - "on_error": { - "type": "string", - "description": "Defines behavior on loop item failure.", - "enum": ["continue"] - } - }, - "required": ["iterable", "iter_var"], - "additionalProperties": false - }, - "capture": { - "type": "object", - "description": "Captures the output of the step into a variable.", - "properties": { - "as": { - "type": "string", - "description": "The name of the new variable to store the result in." - }, - "transform": { - "type": "string", - "description": "A templating expression to transform the step.output before saving it." - }, - "key": { - "type": "string", - "description": "For loops only: A templating expression to derive the dictionary key for each iteration. Has access to loop variables and 'result'. If not specified, defaults to the iter_var value." - } - }, - "required": ["as"], - "additionalProperties": false - }, - "on_error": { - "type": "array", - "description": "A list of nested steps to execute if the current step fails.", - "items": { - "$ref": "#/definitions/step" - } - }, - "exit_on_error": { - "type": "boolean", - "description": "If False, the scenario will continue even if the step fails and no on_error block is defined.", - "default": true - }, - "parallel": { - "type": "boolean", - "description": "If False, forces the action to run sequentially when targeting multiple hosts.", - "default": true - } - }, - "required": [ - "name", - "action" - ], - "additionalProperties": false - } - } -} diff --git a/scenarios/scenarios_tutorial.md b/scenarios/scenarios_tutorial.md deleted file mode 100644 index cabb4dd4..00000000 --- a/scenarios/scenarios_tutorial.md +++ /dev/null @@ -1,1068 +0,0 @@ -# Broker Scenarios Tutorial - -Scenarios are a powerful feature that allows you to chain multiple Broker actions together in YAML files. Instead of running separate `broker checkout`, `broker checkin`, and shell commands, you can define a complete workflow in a single scenario file. - -This tutorial covers everything you need to know to write effective scenarios. - -## Table of Contents - -1. [Quick Start](#quick-start) -2. [Scenario Structure](#scenario-structure) -3. [Available Actions](#available-actions) -4. [Step Configuration Options](#step-configuration-options) -5. [Variables and Templating](#variables-and-templating) -6. [Host Selection and Inventory Filters](#host-selection-and-inventory-filters) -7. [Loops](#loops) -8. [Error Handling](#error-handling) -9. [Capturing Output](#capturing-output) -10. [Complete Examples](#complete-examples) -11. [CLI Reference](#cli-reference) - ---- - -## Quick Start - -Here's a minimal scenario that checks out a container, runs a command, and checks it back in: - -```yaml -# my_first_scenario.yaml -steps: - - name: Provision a container - action: checkout - arguments: - container_host: ubi8 - - - name: Run a command - action: ssh - arguments: - command: "cat /etc/os-release" - with: - hosts: scenario_inventory - - - name: Release the container - action: checkin - with: - hosts: scenario_inventory -``` - -Save this file to `~/.broker/scenarios/my_first_scenario.yaml`, then run: - -```bash -broker scenarios execute my_first_scenario -``` - ---- - -## Scenario Structure - -A scenario file has three main sections: - -```yaml -# config: Optional global settings -config: - inventory_path: /path/to/custom_inventory.yaml # Custom inventory file - log_path: my_scenario.log # Custom log file - settings: # Provider-specific settings - Container: - runtime: podman - -# variables: Optional key-value pairs for use in steps -variables: - MY_IMAGE: ubi8 - INSTALL_PACKAGES: true - TIMEOUT: 60 - -# steps: Required list of actions to execute -steps: - - name: Step 1 - action: checkout - arguments: - container_host: "{{ MY_IMAGE }}" -``` - -### Config Section - -The `config` section allows you to customize scenario behavior: - -| Key | Description | -|-----|-------------| -| `inventory_path` | Path to a custom inventory file for this scenario | -| `log_path` | Custom log file path (see path resolution rules below) | -| `settings` | Nested map of provider-specific settings that override `broker_settings.yaml` | - -**Log Path Resolution Rules:** -- **Not specified**: Uses the default `broker.log` file in `{BROKER_DIRECTORY}/logs/` -- **Filename only** (e.g., `my_scenario.log`): Creates file in `{BROKER_DIRECTORY}/logs/` -- **Absolute path with filename** (e.g., `/var/log/broker/custom.log`): Uses as-is -- **Absolute directory** (e.g., `/var/log/broker/`): Creates `{scenario_name}.log` in that directory - -**Example: Override AnsibleTower timeout** -```yaml -config: - settings: - AnsibleTower: - workflow_timeout: 600 -``` - -### Variables Section - -Variables defined here are available throughout your scenario via Jinja2 templating: - -```yaml -variables: - RHEL_VERSION: "9.4" - HOST_COUNT: 3 - DEPLOY_CONFIG: - memory: 4096 - cpus: 2 - -steps: - - name: Deploy hosts - action: checkout - arguments: - workflow: deploy-rhel - rhel_version: "{{ RHEL_VERSION }}" - count: "{{ HOST_COUNT }}" -``` - -Variables can be overridden via CLI: -```bash -broker scenarios execute my_scenario --RHEL_VERSION 8.10 --HOST_COUNT 5 -``` - ---- - -## Available Actions - -Scenarios support 11 different actions: - -### `checkout` - Provision Hosts - -Checks out hosts from a provider (VMs, containers, etc.). - -```yaml -- name: Provision RHEL VM - action: checkout - arguments: - workflow: deploy-rhel # AnsibleTower workflow - rhel_version: "9.4" - count: 2 # Number of hosts - note: "Testing new feature" - -- name: Provision container - action: checkout - arguments: - container_host: ubi8 # Container image - ports: "22:2222 80:8080" # Port mappings - environment: "DEBUG=1" # Environment variables -``` - -**Output:** List of host objects, automatically added to `scenario_inventory`. - -### `checkin` - Release Hosts - -Releases hosts back to the provider. - -```yaml -- name: Release all scenario hosts - action: checkin - with: - hosts: scenario_inventory - -- name: Release specific hosts - action: checkin - with: - hosts: "@scenario_inv[0:2]" # First two hosts only -``` - -**Output:** `true` on success. - -### `ssh` - Execute Remote Commands - -Runs shell commands on target hosts. - -```yaml -- name: Check disk space - action: ssh - arguments: - command: "df -h" - timeout: 30 # Optional timeout in seconds - with: - hosts: scenario_inventory -``` - -**Output:** -- Single host: A Result object with `stdout`, `stderr`, and `status` attributes -- Multiple hosts: Dictionary mapping hostname to Result object - -### `scp` - Copy Files to Hosts - -Uploads files to remote hosts. - -```yaml -- name: Upload config file - action: scp - arguments: - source: /local/path/config.yaml - destination: /etc/myapp/config.yaml - with: - hosts: scenario_inventory -``` - -**Output:** Result object with success message. - -### `sftp` - Transfer Files - -Transfers files via SFTP (supports both upload and download). - -```yaml -- name: Upload script - action: sftp - arguments: - source: ./scripts/setup.sh - destination: /root/setup.sh - direction: upload # Default - -- name: Download logs - action: sftp - arguments: - source: /var/log/app.log - destination: ./logs/app.log - direction: download - with: - hosts: scenario_inventory -``` - -**Output:** Result object with success message. - -### `execute` - Run Provider Actions - -Executes arbitrary provider actions (like power operations, extend lease, etc.). - -```yaml -- name: Extend VM lease - action: execute - arguments: - workflow: extend-lease - source_vm: "{{ host.name }}" - extend_days: 7 -``` - -**Output:** Provider-specific result. - -### `provider_info` - Query Provider Resources - -Queries a provider for available resources (workflows, images, inventories, etc.). - -```yaml -# Flag-style query: list all workflows -- name: List available workflows - action: provider_info - arguments: - provider: AnsibleTower - query: workflows - tower_inventory: my-inventory - -# Value-style query: get specific workflow details -- name: Get workflow details - action: provider_info - arguments: - provider: AnsibleTower - query: - workflow: deploy-rhel -``` - -**Available queries by provider:** - -| Provider | Flag Queries | Value Queries | -|----------|--------------|---------------| -| AnsibleTower | `workflows`, `inventories`, `job_templates`, `templates`, `flavors` | `workflow`, `inventory`, `job_template` | -| Container | `container_hosts`, `container_apps` | `container_host`, `container_app` | -| Beaker | `jobs` | `job` | -| Foreman | `hostgroups` | `hostgroup` | -| OpenStack | `images`, `flavors`, `networks`, `templates` | - | - -**Output:** Dictionary or list of provider resource data. - -### `inventory` - Query or Sync Inventory - -Works with Broker's inventory system. - -```yaml -# Sync inventory from a provider -- name: Sync Tower inventory - action: inventory - arguments: - sync: AnsibleTower - -# Filter inventory -- name: Get RHEL hosts - action: inventory - arguments: - filter: "'rhel' in @inv.name" -``` - -**Output:** Inventory data (list of host dictionaries). - -### `output` - Write Content - -Writes content to stdout, stderr, or a file. - -```yaml -# Write to stdout -- name: Display message - action: output - arguments: - content: "Deployment complete!" - destination: stdout # Default - -# Write to file (format auto-detected by extension) -- name: Save results to JSON - action: output - arguments: - content: "{{ workflow_results }}" - destination: /tmp/results.json - mode: overwrite # or "append" - -# Write to YAML file -- name: Save hosts list - action: output - arguments: - content: "{{ scenario_inventory }}" - destination: ./hosts.yaml -``` - -**Output:** The content that was written. - -### `exit` - Exit Scenario - -Terminates scenario execution with a return code. - -```yaml -- name: Exit on failure condition - action: exit - arguments: - return_code: 1 - message: "Required condition not met" - when: not deployment_successful - -- name: Exit successfully - action: exit - arguments: - return_code: 0 - message: "All tests passed" -``` - -### `run_scenarios` - Execute Other Scenarios - -Chains other scenario files for modular workflows. - -```yaml -- name: Run cleanup scenarios - action: run_scenarios - arguments: - paths: - - /path/to/cleanup_vms.yaml - - /path/to/cleanup_containers.yaml -``` - -**Output:** List of `{path, success}` dictionaries. - ---- - -## Step Configuration Options - -Each step supports several configuration options: - -### `name` (required) - -Human-readable identifier for the step. Used for logging and step memory references. - -```yaml -- name: Deploy production servers -``` - -### `action` (required) - -One of the 11 available actions. - -### `arguments` (optional) - -Key-value pairs passed to the action. Supports templating. - -### `with` - Target Host Selection - -Specifies which hosts an action should target. - -```yaml -with: - hosts: scenario_inventory # All hosts from this scenario - hosts: inventory # All hosts from main Broker inventory - hosts: "@scenario_inv[0]" # First scenario host - hosts: "'rhel' in @inv.name" # Filtered hosts -``` - -### `when` - Conditional Execution - -Execute step only if condition is true. - -```yaml -- name: Install packages - action: ssh - arguments: - command: "dnf install -y httpd" - with: - hosts: scenario_inventory - when: INSTALL_PACKAGES == true - -- name: Cleanup only if failed - action: checkin - with: - hosts: scenario_inventory - when: previous_step.status == 'failed' -``` - -### `parallel` - Control Parallel Execution - -For multi-host actions, control whether they run in parallel or sequentially. - -```yaml -- name: Run migrations sequentially - action: ssh - arguments: - command: "./migrate.sh" - with: - hosts: scenario_inventory - parallel: false # Run one at a time (default: true) -``` - -### `exit_on_error` - Continue on Failure - -By default, scenarios stop on step failure. Set to `false` to continue. - -```yaml -- name: Optional cleanup - action: ssh - arguments: - command: "rm -rf /tmp/cache" - with: - hosts: scenario_inventory - exit_on_error: false # Continue even if this fails -``` - ---- - -## Variables and Templating - -Scenarios use Jinja2 templating for dynamic values. - -### Template Syntax - -```yaml -# Simple variable -"{{ variable_name }}" - -# Attribute access -"{{ host.hostname }}" - -# Method calls -"{{ result.stdout.strip() }}" - -# String interpolation -"Host {{ hostname }} returned: {{ result.stdout }}" - -# Expressions -"{{ count * 2 }}" -``` - -### Available Context Variables - -| Variable | Description | -|----------|-------------| -| `step` | Current step's memory (name, output, status) | -| `previous_step` | Previous step's memory | -| `steps` | Dictionary of all steps by name | -| `scenario_inventory` | List of hosts checked out by this scenario | -| User variables | Any variable from `variables` section or CLI | -| Captured variables | Any variable captured via `capture` | - -### Step Memory Attributes - -Access previous step results: - -```yaml -- name: Run command - action: ssh - arguments: - command: "ls /root" - with: - hosts: scenario_inventory - capture: - as: ls_result - -- name: Check if file exists - action: ssh - arguments: - command: "cat /root/config.yaml" - with: - hosts: scenario_inventory - when: "'config.yaml' in ls_result.stdout" -``` - -Step memory has these attributes: -- `name` - Step name -- `output` - Action result -- `status` - "pending", "running", "completed", "skipped", or "failed" - ---- - -## Host Selection and Inventory Filters - -### Inventory References - -| Expression | Description | -|------------|-------------| -| `scenario_inventory` | All hosts checked out by this scenario | -| `inventory` | All hosts from main Broker inventory | -| `@scenario_inv` | Scenario inventory (for filtering) | -| `@inv` | Main inventory (for filtering) | - -### Filter Expressions - -Filter hosts using Python-like expressions: - -```yaml -# Index access -"@scenario_inv[0]" # First host -"@scenario_inv[-1]" # Last host - -# Slicing -"@scenario_inv[0:3]" # First three hosts -"@scenario_inv[1:]" # All except first -"@inv[:]" # All hosts (as list) - -# Attribute filtering -"'rhel' in @inv.name" # Hosts with 'rhel' in name -"'satellite' in @inv.hostname" # Hosts with 'satellite' in hostname -"@inv._broker_provider == 'AnsibleTower'" # By provider -``` - ---- - -## Loops - -Execute a step multiple times over an iterable. - -### Basic Loop - -```yaml -- name: Process each host - action: ssh - arguments: - command: "hostname" - loop: - iterable: "@scenario_inv[:]" # Loop over all scenario hosts - iter_var: current_host - with: - hosts: "{{ current_host }}" # Use loop variable -``` - -### Loop Over Variables - -```yaml -variables: - PACKAGES: - - httpd - - postgresql - - redis - -steps: - - name: Install packages - action: ssh - arguments: - command: "dnf install -y {{ package }}" - loop: - iterable: PACKAGES - iter_var: package - with: - hosts: scenario_inventory -``` - -### Loop with Dictionary Items - -Use tuple unpacking to iterate over dictionary items: - -```yaml -- name: Get command output from each host - action: ssh - arguments: - command: "uptime" - with: - hosts: scenario_inventory - capture: - as: uptime_results # Dict: {hostname: Result} - -- name: Process each result - action: output - arguments: - content: "{{ hostname }}: {{ result.stdout }}" - destination: stdout - loop: - iterable: uptime_results.items() - iter_var: hostname, result # Tuple unpacking -``` - -### Loop with Conditional - -The `when` condition is evaluated for each iteration: - -```yaml -- name: Only process successful results - action: output - arguments: - content: "{{ hostname }} is healthy" - loop: - iterable: check_results.items() - iter_var: hostname, result - when: result.status == 0 -``` - -### Loop Error Handling - -Continue loop even if some iterations fail: - -```yaml -- name: Run risky command on each host - action: ssh - arguments: - command: "risky-operation" - loop: - iterable: "@scenario_inv[:]" - iter_var: host - on_error: continue # Don't stop on failure - with: - hosts: "{{ host }}" -``` - ---- - -## Error Handling - -### Simple Continue - -Proceed to next step even if current step fails: - -```yaml -- name: Optional step - action: ssh - arguments: - command: "optional-command" - with: - hosts: scenario_inventory - on_error: continue -``` - -### Recovery Steps - -Execute cleanup or recovery actions when a step fails: - -```yaml -- name: Critical operation - action: ssh - arguments: - command: "critical-operation" - with: - hosts: scenario_inventory - on_error: - - name: Log failure - action: output - arguments: - content: "Critical operation failed, cleaning up..." - destination: stderr - - - name: Cleanup resources - action: checkin - with: - hosts: scenario_inventory - - - name: Exit with error - action: exit - arguments: - return_code: 1 - message: "Critical operation failed" -``` - -### Exit on Error Control - -For non-critical steps without recovery: - -```yaml -- name: Try to gather metrics - action: ssh - arguments: - command: "collect-metrics" - with: - hosts: scenario_inventory - exit_on_error: false # Continue regardless of outcome -``` - ---- - -## Capturing Output - -Store step results in variables for later use. - -### Basic Capture - -```yaml -- name: Get hostname - action: ssh - arguments: - command: "hostname -f" - with: - hosts: scenario_inventory - capture: - as: hostname_result - -- name: Display hostname - action: output - arguments: - content: "FQDN: {{ hostname_result.stdout }}" -``` - -### Capture with Transform - -Extract or transform the output before storing: - -```yaml -- name: Get OS version - action: ssh - arguments: - command: "cat /etc/redhat-release" - with: - hosts: scenario_inventory - capture: - as: os_version - transform: "{{ step.output.stdout.strip() }}" -``` - -### Capture in Loops - -When capturing loop results, each iteration's result is stored in a dictionary: - -```yaml -- name: Check each service - action: ssh - arguments: - command: "systemctl is-active {{ service }}" - loop: - iterable: SERVICES - iter_var: service - with: - hosts: scenario_inventory - capture: - as: service_status # Dict: {"httpd": Result, "nginx": Result, ...} -``` - -Use custom keys for better organization: - -```yaml -- name: Get workflow details - action: provider_info - arguments: - provider: AnsibleTower - query: - workflow: "{{ workflow_name }}" - loop: - iterable: workflow_list - iter_var: workflow_name - capture: - as: workflow_details - key: result.name # Use workflow name as dict key -``` - ---- - -## Complete Examples - -### Example 1: CI/CD Pipeline Test - -```yaml -# ci_pipeline_test.yaml -# Tests deployment and runs verification on containers - -variables: - TEST_IMAGE: ubi9 - HOST_COUNT: 2 - TEST_PACKAGES: - - python3 - - git - - make - -config: - inventory_path: ~/.broker/ci_test_inventory.yaml - -steps: - - name: Provision test containers - action: checkout - arguments: - container_host: "{{ TEST_IMAGE }}" - count: "{{ HOST_COUNT }}" - - - name: Install test dependencies - action: ssh - arguments: - command: "dnf install -y {{ TEST_PACKAGES | join(' ') }}" - with: - hosts: scenario_inventory - parallel: true - - - name: Clone test repository - action: ssh - arguments: - command: "git clone https://github.com/example/tests.git /root/tests" - with: - hosts: scenario_inventory - - - name: Run tests - action: ssh - arguments: - command: "cd /root/tests && make test" - timeout: 300 - with: - hosts: scenario_inventory - capture: - as: test_results - - - name: Save test results - action: output - arguments: - content: "{{ test_results }}" - destination: ./test_results.yaml - - - name: Cleanup containers - action: checkin - with: - hosts: scenario_inventory -``` - -### Example 2: Multi-Provider Inventory Management - -```yaml -# sync_and_report.yaml -# Syncs inventory from multiple providers and generates a report - -variables: - PROVIDERS: - - AnsibleTower - - Container - - Beaker - -steps: - - name: Sync all provider inventories - action: inventory - arguments: - sync: "{{ provider }}" - loop: - iterable: PROVIDERS - iter_var: provider - on_error: continue - capture: - as: sync_results - - - name: Load full inventory - action: inventory - arguments: {} - capture: - as: full_inventory - - - name: Generate inventory report - action: output - arguments: - content: | - # Broker Inventory Report - Generated: {{ now }} - Total hosts: {{ full_inventory | length }} - - Hosts by provider: - {% for host in full_inventory %} - - {{ host.hostname }} ({{ host._broker_provider }}) - {% endfor %} - destination: ./inventory_report.md - - - name: Display summary - action: output - arguments: - content: "Synced {{ PROVIDERS | length }} providers. Total hosts: {{ full_inventory | length }}" -``` - -### Example 3: Deployment with Rollback - -```yaml -# deploy_with_rollback.yaml -# Deploys application with automatic rollback on failure - -variables: - WORKFLOW: deploy-application - APP_VERSION: "2.1.0" - ROLLBACK_VERSION: "2.0.0" - -steps: - - name: Deploy new version - action: checkout - arguments: - workflow: "{{ WORKFLOW }}" - app_version: "{{ APP_VERSION }}" - on_error: - - name: Log deployment failure - action: output - arguments: - content: "Deployment of {{ APP_VERSION }} failed, initiating rollback..." - destination: stderr - - - name: Deploy rollback version - action: checkout - arguments: - workflow: "{{ WORKFLOW }}" - app_version: "{{ ROLLBACK_VERSION }}" - on_error: - - name: Critical failure - action: exit - arguments: - return_code: 2 - message: "Both deployment and rollback failed!" - - - name: Rollback successful - action: output - arguments: - content: "Rollback to {{ ROLLBACK_VERSION }} successful" - - - name: Exit with warning - action: exit - arguments: - return_code: 1 - message: "Deployed rollback version due to failure" - - - name: Verify deployment - action: ssh - arguments: - command: "curl -s localhost:8080/health" - with: - hosts: scenario_inventory - capture: - as: health_check - - - name: Deployment complete - action: output - arguments: - content: "Successfully deployed {{ APP_VERSION }}" -``` - ---- - -## CLI Reference - -### List Available Scenarios - -```bash -broker scenarios list -``` - -Shows all scenarios in `~/.broker/scenarios/`. - -### Execute a Scenario - -```bash -# By name (from scenarios directory) -broker scenarios execute my_scenario - -# By path -broker scenarios execute /path/to/scenario.yaml - -# With variable overrides -broker scenarios execute my_scenario --MY_VAR value --COUNT 5 - -# With config overrides -broker scenarios execute my_scenario --config.settings.Container.runtime docker - -# Run in background -broker scenarios execute my_scenario --background -``` - -### Get Scenario Information - -```bash -broker scenarios info my_scenario - -# Without syntax highlighting -broker scenarios info my_scenario --no-syntax -``` - -### Validate a Scenario - -```bash -broker scenarios validate my_scenario -``` - -Checks syntax and schema validation without executing. - ---- - -## Tips and Best Practices - -1. **Always clean up**: Include a `checkin` step at the end of your scenarios, preferably with error handling to ensure cleanup even on failure. - -2. **Use meaningful names**: Step names appear in logs and can be referenced via `steps['Step Name']`. - -3. **Capture intermediate results**: Use `capture` liberally to store results for debugging and conditional logic. - -4. **Test with `--background`**: Long-running scenarios can be run in the background. - -5. **Validate before running**: Use `broker scenarios validate` to catch syntax errors early. - -6. **Modularize complex workflows**: Split large scenarios into smaller ones and use `run_scenarios` to chain them. - -7. **Use variables for flexibility**: Define configurable values in the `variables` section so they can be overridden via CLI. - -8. **Handle errors gracefully**: Use `on_error` blocks for critical steps that need cleanup on failure. - ---- - -## Troubleshooting - -### Common Issues - -**"Undefined variable in template"** -- Check that the variable is defined in `variables` section or captured by a previous step -- Variables are case-sensitive -- Use `broker scenarios info` to see available variables - -**"Scenario not found"** -- Ensure file is in `~/.broker/scenarios/` or provide full path -- File must have `.yaml` or `.yml` extension - -**"SSH action requires target hosts"** -- Add a `with.hosts` specification to the step -- Ensure a previous `checkout` step has added hosts to `scenario_inventory` - -**"Step failed but no on_error defined"** -- Add `on_error: continue` to ignore failures -- Add `exit_on_error: false` to continue without error handling -- Add an `on_error` block with recovery steps - -### Debugging - -Enable verbose logging: -```bash -broker --log-level debug scenarios execute my_scenario -``` - -Check the scenario structure: -```bash -broker scenarios info my_scenario -``` - -Validate without executing: -```bash -broker scenarios validate my_scenario -``` diff --git a/tests/data/scenarios/capture_scenario.yaml b/tests/data/scenarios/capture_scenario.yaml new file mode 100644 index 00000000..be3bde98 --- /dev/null +++ b/tests/data/scenarios/capture_scenario.yaml @@ -0,0 +1,18 @@ +# Scenario for testing capture functionality +variables: + initial_value: start + +steps: + - name: capture_step + action: output + arguments: + content: "captured_output" + destination: stdout + capture: + as: captured_var + + - name: use_captured + action: output + arguments: + content: "Using captured: {{ captured_var }}" + destination: stdout diff --git a/tests/data/scenarios/conditions_scenario.yaml b/tests/data/scenarios/conditions_scenario.yaml new file mode 100644 index 00000000..e11f7176 --- /dev/null +++ b/tests/data/scenarios/conditions_scenario.yaml @@ -0,0 +1,27 @@ +# Scenario for testing conditional execution +variables: + run_this: true + skip_this: false + value: 42 + +steps: + - name: should_run + action: output + arguments: + content: "This step should run" + destination: stdout + when: run_this == true + + - name: should_skip + action: output + arguments: + content: "This step should be skipped" + destination: stdout + when: skip_this == true + + - name: conditional_value + action: output + arguments: + content: "Value is greater than 40" + destination: stdout + when: value > 40 diff --git a/tests/data/scenarios/invalid_schema.yaml b/tests/data/scenarios/invalid_schema.yaml new file mode 100644 index 00000000..385f82c3 --- /dev/null +++ b/tests/data/scenarios/invalid_schema.yaml @@ -0,0 +1,8 @@ +# Invalid scenario - missing required 'steps' field +config: + inventory_path: /tmp/test.yaml + +variables: + foo: bar + +# 'steps' is missing - should fail schema validation diff --git a/tests/data/scenarios/loop_scenario.yaml b/tests/data/scenarios/loop_scenario.yaml new file mode 100644 index 00000000..f6234da5 --- /dev/null +++ b/tests/data/scenarios/loop_scenario.yaml @@ -0,0 +1,28 @@ +# Scenario for testing loop functionality +variables: + test_items: + - alpha + - beta + - gamma + test_dict: + key1: value1 + key2: value2 + +steps: + - name: loop_step + action: output + arguments: + content: "Processing {{ item }}" + destination: stdout + loop: + iterable: test_items + iter_var: item + + - name: dict_loop_step + action: output + arguments: + content: "Key: {{ k }}, Value: {{ v }}" + destination: stdout + loop: + iterable: test_dict.items() + iter_var: k, v diff --git a/tests/data/scenarios/valid_scenario.yaml b/tests/data/scenarios/valid_scenario.yaml new file mode 100644 index 00000000..14077851 --- /dev/null +++ b/tests/data/scenarios/valid_scenario.yaml @@ -0,0 +1,21 @@ +# A valid minimal scenario for testing ScenarioRunner initialization +config: + inventory_path: /tmp/test_inventory.yaml + log_path: test_scenario.log + settings: + Container: + runtime: podman + +variables: + my_var: test_value + my_list: + - item1 + - item2 + my_count: 2 + enabled: true + +steps: + - name: test_step + action: checkout + arguments: + nick: test_nick diff --git a/tests/functional/scenarios/comprehensive_container_test.yaml b/tests/functional/scenarios/comprehensive_container_test.yaml index fe350fc1..32547dee 100644 --- a/tests/functional/scenarios/comprehensive_container_test.yaml +++ b/tests/functional/scenarios/comprehensive_container_test.yaml @@ -175,7 +175,7 @@ steps: with: hosts: "@scenario_inv[0]" - - name: Use SSH to copy file between containers (simulating SCP workflow) + - name: Use SSH to create files on containers action: ssh arguments: command: "echo 'SCP simulation - file created via SSH' > /tmp/scp_test_file.txt" diff --git a/tests/functional/test_scenarios.py b/tests/functional/test_scenarios.py index 52625140..9215f3d3 100644 --- a/tests/functional/test_scenarios.py +++ b/tests/functional/test_scenarios.py @@ -24,4 +24,7 @@ def test_deploy_details_scenario(): assert scenario is not None assert scenario.scenario_name == "deploy_details" - scenario.run() \ No newline at end of file + scenario.run() + assert "workflow_list" in scenario.variables + assert "workflow_details" in scenario.variables + assert scenario.steps_memory["List workflows"].status == "completed" diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 00000000..6c4bc7fb --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,397 @@ +"""Unit tests for the broker.scenarios module. + +These tests cover the pure utility functions, StepMemory class, and +ScenarioRunner initialization/configuration without requiring actual +providers or external services. +""" + +from pathlib import Path + +import pytest + +from broker import scenarios +from broker.exceptions import ScenarioError +from broker.helpers import MockStub + + +TEST_SCENARIOS_DIR = Path(__file__).parent / "data" / "scenarios" +VALID_SCENARIO_PATH = TEST_SCENARIOS_DIR / "valid_scenario.yaml" +INVALID_SCHEMA_PATH = TEST_SCENARIOS_DIR / "invalid_schema.yaml" +LOOP_SCENARIO_PATH = TEST_SCENARIOS_DIR / "loop_scenario.yaml" + +SAMPLE_CONTEXT = { + "my_var": "test_value", + "my_list": ["a", "b", "c"], + "my_dict": {"key1": "val1", "key2": "val2"}, + "my_int": 42, + "my_bool": True, + "nested": {"inner": {"value": "deep"}}, +} + + +def test_get_schema_returns_valid_dict(): + """Schema should be loaded as a dictionary with required 'steps' field.""" + schema = scenarios.get_schema() + assert schema is not None + assert isinstance(schema, dict) + assert "required" in schema + assert "steps" in schema["required"] + + +def test_render_template_preserves_python_types(): + """Simple variable references should preserve Python types (int, bool, list, dict).""" + assert scenarios.render_template("{{ my_int }}", SAMPLE_CONTEXT) == 42 + assert scenarios.render_template("{{ my_bool }}", SAMPLE_CONTEXT) is True + assert scenarios.render_template("{{ my_list }}", SAMPLE_CONTEXT) == ["a", "b", "c"] + assert scenarios.render_template("{{ my_dict }}", SAMPLE_CONTEXT) == {"key1": "val1", "key2": "val2"} + + +def test_render_template_complex_returns_string(): + """Templates with surrounding text should return rendered string.""" + result = scenarios.render_template("Value is: {{ my_var }}", SAMPLE_CONTEXT) + assert result == "Value is: test_value" + assert isinstance(result, str) + + +def test_render_template_passthrough(): + """Non-string input and strings without template syntax should pass through unchanged.""" + assert scenarios.render_template(42, SAMPLE_CONTEXT) == 42 + assert scenarios.render_template(None, SAMPLE_CONTEXT) is None + assert scenarios.render_template("plain string", SAMPLE_CONTEXT) == "plain string" + + +def test_render_template_undefined_behavior(): + """Complex templates with undefined vars raise; simple refs return None.""" + # Complex templates (with text around variable) raise ScenarioError + with pytest.raises(ScenarioError) as exc_info: + scenarios.render_template("prefix {{ undefined_var }}", SAMPLE_CONTEXT) + assert "Undefined variable" in str(exc_info.value) + + # Simple variable refs use evaluate_expression path, which returns None + result = scenarios.render_template("{{ undefined_var }}", SAMPLE_CONTEXT) + assert result is None + + +def test_evaluate_expression_returns_python_objects(): + """Expressions should return actual Python objects, not strings.""" + assert scenarios.evaluate_expression("my_int", SAMPLE_CONTEXT) == 42 + assert scenarios.evaluate_expression("my_list", SAMPLE_CONTEXT) == ["a", "b", "c"] + assert scenarios.evaluate_expression("my_int + 10", SAMPLE_CONTEXT) == 52 + assert list(scenarios.evaluate_expression("my_dict.items()", SAMPLE_CONTEXT)) == [ + ("key1", "val1"), + ("key2", "val2"), + ] + + +def test_evaluate_expression_undefined_returns_none(): + """Undefined variable returns None (jinja2 compile_expression behavior).""" + result = scenarios.evaluate_expression("undefined", SAMPLE_CONTEXT) + assert result is None + + +def test_evaluate_condition_boolean_results(): + """Conditions should evaluate to proper boolean values.""" + assert scenarios.evaluate_condition("", SAMPLE_CONTEXT) is True + assert scenarios.evaluate_condition(None, SAMPLE_CONTEXT) is True + assert scenarios.evaluate_condition("my_bool == true", SAMPLE_CONTEXT) is True + assert scenarios.evaluate_condition("my_int > 40", SAMPLE_CONTEXT) is True + assert scenarios.evaluate_condition("my_int < 40", SAMPLE_CONTEXT) is False + + +def test_evaluate_condition_string_coercion(): + """String values like 'true', 'false', 'yes', 'no' should coerce to booleans.""" + assert scenarios.evaluate_condition("result", {"result": "true"}) is True + assert scenarios.evaluate_condition("result", {"result": "yes"}) is True + assert scenarios.evaluate_condition("result", {"result": "false"}) is False + assert scenarios.evaluate_condition("result", {"result": "none"}) is False + + +def test_evaluate_condition_is_defined(): + """'is defined' checks should work for variable existence.""" + assert scenarios.evaluate_condition("my_var is defined", SAMPLE_CONTEXT) is True + assert scenarios.evaluate_condition("undefined_var is defined", SAMPLE_CONTEXT) is False + + +def test_recursive_render_nested_structures(): + """Should render templates throughout nested dicts and lists.""" + data = { + "outer": { + "inner": "{{ my_var }}", + "list": ["{{ my_int }}", {"deep": "{{ my_bool }}"}], + }, + "static": "no change", + } + result = scenarios.recursive_render(data, SAMPLE_CONTEXT) + + assert result["outer"]["inner"] == "test_value" + assert result["outer"]["list"][0] == 42 + assert result["outer"]["list"][1]["deep"] is True + assert result["static"] == "no change" + + +def test_find_scenario_by_direct_path(): + """Should find scenario by direct file path.""" + result = scenarios.find_scenario(str(VALID_SCENARIO_PATH)) + assert result == VALID_SCENARIO_PATH + + +def test_find_scenario_in_scenarios_dir(tmp_path, monkeypatch): + """Should find scenarios by name in SCENARIOS_DIR with .yaml or .yml extension.""" + scenarios_dir = tmp_path / "scenarios" + scenarios_dir.mkdir() + (scenarios_dir / "test_scenario.yaml").write_text("steps: []") + (scenarios_dir / "another.yml").write_text("steps: []") + monkeypatch.setattr(scenarios, "SCENARIOS_DIR", scenarios_dir) + + assert scenarios.find_scenario("test_scenario") == scenarios_dir / "test_scenario.yaml" + assert scenarios.find_scenario("another") == scenarios_dir / "another.yml" + + +def test_find_scenario_nonexistent_raises(): + """Should raise ScenarioError for nonexistent scenario.""" + with pytest.raises(ScenarioError) as exc_info: + scenarios.find_scenario("does_not_exist_anywhere") + assert "Scenario not found" in str(exc_info.value) + + +def test_list_scenarios_returns_sorted_names(tmp_path, monkeypatch): + """Should return sorted list of scenario names without extensions.""" + scenarios_dir = tmp_path / "scenarios" + scenarios_dir.mkdir() + (scenarios_dir / "zebra.yaml").write_text("steps: []") + (scenarios_dir / "alpha.yml").write_text("steps: []") + monkeypatch.setattr(scenarios, "SCENARIOS_DIR", scenarios_dir) + + result = scenarios.list_scenarios() + assert result == ["alpha", "zebra"] + + +def test_validate_scenario_valid(): + """Valid scenario should pass validation.""" + is_valid, error = scenarios.validate_scenario(VALID_SCENARIO_PATH) + assert is_valid is True + assert error is None + + +def test_validate_scenario_invalid_and_missing(): + """Invalid schema and nonexistent files should fail validation.""" + is_valid, error = scenarios.validate_scenario(INVALID_SCHEMA_PATH) + assert is_valid is False + assert "Validation error" in error + + is_valid, error = scenarios.validate_scenario("/nonexistent/path.yaml") + assert is_valid is False + assert "not found" in error + + +def test_step_memory_initialization_and_access(): + """StepMemory should initialize with defaults and support dict-style access.""" + mem = scenarios.StepMemory("test_step") + + # Check defaults + assert mem.name == "test_step" + assert mem.output is None + assert mem.status == "pending" + + # Update and check to_dict + mem.output = "some_output" + mem.status = "completed" + assert mem.to_dict() == {"name": "test_step", "output": "some_output", "status": "completed"} + + # Dict-style access + assert mem["name"] == "test_step" + assert mem["nonexistent"] is None + assert mem.get("nonexistent", "default") == "default" + + +def test_scenario_runner_init_valid(): + """Should initialize with valid scenario file and load config/variables.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + + assert runner.scenario_name == "valid_scenario" + assert runner.variables["my_var"] == "test_value" + assert runner.variables["my_count"] == 2 + assert runner.config.get("inventory_path") == "/tmp/test_inventory.yaml" + + +def test_scenario_runner_cli_vars_override(): + """CLI variables should override scenario variables.""" + runner = scenarios.ScenarioRunner( + VALID_SCENARIO_PATH, + cli_vars={"my_var": "overridden", "new_var": "new_value"}, + ) + + assert runner.variables["my_var"] == "overridden" + assert runner.variables["new_var"] == "new_value" + assert runner.variables["my_count"] == 2 # Unchanged + + +def test_scenario_runner_cli_config_overrides(): + """CLI config should create/update nested config paths.""" + runner = scenarios.ScenarioRunner( + VALID_SCENARIO_PATH, + cli_config={"settings.Container.runtime": "docker", "settings.NewProvider.option": "value"}, + ) + + assert runner.config["settings"]["Container"]["runtime"] == "docker" + assert runner.config["settings"]["NewProvider"]["option"] == "value" + + +def test_scenario_runner_init_errors(): + """Should raise ScenarioError for nonexistent file or invalid schema.""" + with pytest.raises(ScenarioError) as exc_info: + scenarios.ScenarioRunner(Path("/nonexistent/scenario.yaml")) + assert "Scenario file not found" in str(exc_info.value) + + with pytest.raises(ScenarioError) as exc_info: + scenarios.ScenarioRunner(INVALID_SCHEMA_PATH) + assert "validation failed" in str(exc_info.value) + + +def test_scenario_runner_map_argument_names(): + """'count' should be mapped to '_count', other args unchanged.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + + result = runner._map_argument_names({"count": 5, "nick": "test_nick"}) + + assert result == {"_count": 5, "nick": "test_nick"} + assert "count" not in result + + +def test_scenario_runner_get_inventory_path(tmp_path): + """Should use config inventory_path or generate default.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + assert runner.inventory_path == Path("/tmp/test_inventory.yaml") + + # Scenario without inventory_path config + scenario = tmp_path / "no_inv.yaml" + scenario.write_text("steps:\n - name: test\n action: output\n arguments:\n content: test\n destination: stdout") + runner = scenarios.ScenarioRunner(scenario) + assert "scenario_no_inv_inventory.yaml" in str(runner.inventory_path) + + +def test_scenario_runner_build_context(): + """Context should include variables, steps memory, and scenario_inventory.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + runner.steps_memory["step1"] = scenarios.StepMemory("step1") + runner.steps_memory["step1"].output = "step1_output" + + context = runner._build_context("step2", runner.steps_memory["step1"]) + + assert context["my_var"] == "test_value" + assert context["steps"]["step1"].output == "step1_output" + assert context["previous_step"].name == "step1" + assert context["scenario_inventory"] == [] + + +def test_scenario_runner_capture_output(): + """Capture should store results in variables, with optional transform.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + runner.steps_memory["test"] = scenarios.StepMemory("test") + context = runner._build_context("test", None) + + # Simple capture + runner._capture_output({"as": "my_result"}, "captured_value", context) + assert runner.variables["my_result"] == "captured_value" + + # Capture with transform + result = {"data": [1, 2, 3]} + runner._capture_output( + {"as": "data_length", "transform": "{{ step.output.data | length }}"}, + result, + context, + ) + assert runner.variables["data_length"] == 3 + + +def test_scenario_runner_get_info(): + """get_info should return scenario metadata summary.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + info = runner.get_info() + + assert info["name"] == "valid_scenario" + assert "config" in info + assert "variables" in info + assert info["steps"][0] == {"name": "test_step", "action": "checkout"} + + +def test_resolve_hosts_reference(): + """Should resolve various host reference formats.""" + scenario_inv = [MockStub({"hostname": "host1"}), MockStub({"hostname": "host2"})] + + # 'scenario_inventory' returns a copy + result = scenarios.resolve_hosts_reference("scenario_inventory", scenario_inv, {}) + assert len(result) == 2 + assert result is not scenario_inv + + # Variable that is a list + host_list = [{"hostname": "h1"}, {"hostname": "h2"}] + result = scenarios.resolve_hosts_reference("{{ my_hosts }}", [], {"my_hosts": host_list}) + assert result == host_list + + # Unknown reference returns empty list + result = scenarios.resolve_hosts_reference("unknown_ref", [], {}) + assert result == [] + + +def test_action_output(capsys, tmp_path): + """Output action should write to stdout, stderr, or files.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + + # stdout + runner._action_output({"content": "Hello!", "destination": "stdout"}) + assert "Hello!" in capsys.readouterr().out + + # stderr + runner._action_output({"content": "Error!", "destination": "stderr"}) + assert "Error!" in capsys.readouterr().err + + # File with dict content (JSON) + output_file = tmp_path / "output.json" + runner._action_output({"content": {"key": "value"}, "destination": str(output_file)}) + import json + assert json.loads(output_file.read_text()) == {"key": "value"} + + # Missing content raises + with pytest.raises(ScenarioError) as exc_info: + runner._action_output({"destination": "stdout"}) + assert "requires 'content'" in str(exc_info.value) + + +def test_action_exit(): + """Exit action should raise SystemExit(0) or ScenarioError for non-zero.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + + # Test successful exit with return code 0 + with pytest.raises(SystemExit) as exc_info: + runner._action_exit({"return_code": 0, "message": "Success"}) + assert exc_info.value.code == 0 + + # Test failure with return code 1 + with pytest.raises(ScenarioError) as exc_info: + runner._action_exit({"return_code": 1, "message": "Failure"}) + assert "Failure" in str(exc_info.value) + + # Test other non-zero return code + with pytest.raises(ScenarioError) as exc_info: + runner._action_exit({"return_code": 42, "message": "Custom error"}) + assert "Custom error" in str(exc_info.value) + + # Test default return code (should default to 0) + with pytest.raises(SystemExit) as exc_info: + runner._action_exit({}) + assert exc_info.value.code == 0 + + # Test without message + with pytest.raises(SystemExit) as exc_info: + runner._action_exit({"return_code": 0}) + assert exc_info.value.code == 0 + + +def test_dispatch_action_unknown(): + """Unknown action should raise ScenarioError.""" + runner = scenarios.ScenarioRunner(VALID_SCENARIO_PATH) + + with pytest.raises(ScenarioError) as exc_info: + runner._dispatch_action({"name": "test", "action": "nonexistent"}, {}, None) + assert "Unknown action" in str(exc_info.value) diff --git a/tox.toml b/tox.toml index 1243b565..fd1334e1 100644 --- a/tox.toml +++ b/tox.toml @@ -225,6 +225,7 @@ commands = [ "-v", "{tox_root}/tests/test_broker.py", "{tox_root}/tests/test_helpers.py", + "{tox_root}/tests/test_scenarios.py", "{posargs}", ], ]